Repository: HypothesisWorks/hypothesis Branch: master Commit: 9fe714de9b90 Files: 666 Total size: 4.6 MB Directory structure: gitextract_nlv4f5th/ ├── .claude/ │ ├── CLAUDE.md │ └── commands/ │ └── hypothesis.md ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── CODE_OF_CONDUCT.rst │ ├── actions/ │ │ └── install-base/ │ │ └── action.yml │ └── workflows/ │ ├── fuzz.yml │ ├── main.yml │ ├── update-deps.yml │ └── website.yml ├── .gitignore ├── .readthedocs.yml ├── AUTHORS.rst ├── CITATION.cff ├── CONTRIBUTING.rst ├── LICENSE.txt ├── README.md ├── brand/ │ ├── README.md │ ├── hypothesis.gpl │ ├── hypothesis.sketch │ └── hypothesis2.sketch ├── build.sh ├── guides/ │ ├── README.md │ ├── api-style.rst │ ├── documentation.rst │ ├── internals.rst │ ├── review.rst │ ├── strategies-that-shrink.rst │ └── testing-hypothesis.rst ├── hypothesis-python/ │ ├── LICENSE.txt │ ├── README.md │ ├── RELEASE-sample.rst │ ├── benchmark/ │ │ ├── README.md │ │ ├── graph.py │ │ └── spec.json │ ├── docs/ │ │ ├── _ext/ │ │ │ ├── hypothesis_linkcheck.py │ │ │ └── hypothesis_redirects.py │ │ ├── _static/ │ │ │ ├── better-signatures.css │ │ │ ├── dark-fix.css │ │ │ ├── no-scroll.css │ │ │ └── wrap-in-tables.css │ │ ├── changelog.rst │ │ ├── community.rst │ │ ├── compatibility.rst │ │ ├── conf.py │ │ ├── development.rst │ │ ├── explanation/ │ │ │ ├── domain.rst │ │ │ ├── example-count.rst │ │ │ └── index.rst │ │ ├── extensions.rst │ │ ├── extras.rst │ │ ├── how-to/ │ │ │ ├── custom-database.rst │ │ │ ├── detect-hypothesis-tests.rst │ │ │ ├── external-fuzzers.rst │ │ │ ├── index.rst │ │ │ ├── suppress-healthchecks.rst │ │ │ └── type-strategies.rst │ │ ├── index.rst │ │ ├── packaging.rst │ │ ├── prolog.rst │ │ ├── quickstart.rst │ │ ├── redirect.html.template │ │ ├── reference/ │ │ │ ├── api.rst │ │ │ ├── index.rst │ │ │ ├── integrations.rst │ │ │ ├── internals.rst │ │ │ ├── schema_metadata.json │ │ │ ├── schema_metadata_choices.json │ │ │ ├── schema_observations.json │ │ │ └── strategies.rst │ │ ├── stateful.rst │ │ ├── tutorial/ │ │ │ ├── adapting-strategies.rst │ │ │ ├── adding-notes.rst │ │ │ ├── builtin-strategies.rst │ │ │ ├── custom-strategies.rst │ │ │ ├── flaky.rst │ │ │ ├── index.rst │ │ │ ├── introduction.rst │ │ │ ├── replaying-failures.rst │ │ │ └── settings.rst │ │ └── usage.rst │ ├── examples/ │ │ ├── README.md │ │ ├── example_hypothesis_entrypoint/ │ │ │ ├── example_hypothesis_entrypoint.py │ │ │ ├── setup.py │ │ │ └── test_entrypoint.py │ │ ├── test_basic.py │ │ ├── test_binary_search.py │ │ └── test_rle.py │ ├── pyproject.toml │ ├── pyrightconfig.json │ ├── scripts/ │ │ ├── basic-test.sh │ │ ├── other-tests.sh │ │ └── validate_branch_check.py │ ├── src/ │ │ ├── _hypothesis_ftz_detector.py │ │ ├── _hypothesis_globals.py │ │ ├── _hypothesis_pytestplugin.py │ │ └── hypothesis/ │ │ ├── __init__.py │ │ ├── _settings.py │ │ ├── configuration.py │ │ ├── control.py │ │ ├── core.py │ │ ├── database.py │ │ ├── entry_points.py │ │ ├── errors.py │ │ ├── extra/ │ │ │ ├── __init__.py │ │ │ ├── _array_helpers.py │ │ │ ├── _patching.py │ │ │ ├── array_api.py │ │ │ ├── cli.py │ │ │ ├── codemods.py │ │ │ ├── dateutil.py │ │ │ ├── django/ │ │ │ │ ├── __init__.py │ │ │ │ ├── _fields.py │ │ │ │ └── _impl.py │ │ │ ├── dpcontracts.py │ │ │ ├── ghostwriter.py │ │ │ ├── lark.py │ │ │ ├── numpy.py │ │ │ ├── pandas/ │ │ │ │ ├── __init__.py │ │ │ │ └── impl.py │ │ │ ├── pytestplugin.py │ │ │ ├── pytz.py │ │ │ └── redis.py │ │ ├── internal/ │ │ │ ├── __init__.py │ │ │ ├── cache.py │ │ │ ├── cathetus.py │ │ │ ├── charmap.py │ │ │ ├── compat.py │ │ │ ├── conjecture/ │ │ │ │ ├── __init__.py │ │ │ │ ├── choice.py │ │ │ │ ├── data.py │ │ │ │ ├── datatree.py │ │ │ │ ├── engine.py │ │ │ │ ├── floats.py │ │ │ │ ├── junkdrawer.py │ │ │ │ ├── optimiser.py │ │ │ │ ├── pareto.py │ │ │ │ ├── provider_conformance.py │ │ │ │ ├── providers.py │ │ │ │ ├── shrinker.py │ │ │ │ ├── shrinking/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── bytes.py │ │ │ │ │ ├── choicetree.py │ │ │ │ │ ├── collection.py │ │ │ │ │ ├── common.py │ │ │ │ │ ├── floats.py │ │ │ │ │ ├── integer.py │ │ │ │ │ ├── ordering.py │ │ │ │ │ └── string.py │ │ │ │ └── utils.py │ │ │ ├── constants_ast.py │ │ │ ├── coverage.py │ │ │ ├── detection.py │ │ │ ├── entropy.py │ │ │ ├── escalation.py │ │ │ ├── filtering.py │ │ │ ├── floats.py │ │ │ ├── healthcheck.py │ │ │ ├── intervalsets.py │ │ │ ├── lambda_sources.py │ │ │ ├── observability.py │ │ │ ├── reflection.py │ │ │ ├── scrutineer.py │ │ │ └── validation.py │ │ ├── provisional.py │ │ ├── py.typed │ │ ├── reporting.py │ │ ├── stateful.py │ │ ├── statistics.py │ │ ├── strategies/ │ │ │ ├── __init__.py │ │ │ └── _internal/ │ │ │ ├── __init__.py │ │ │ ├── attrs.py │ │ │ ├── collections.py │ │ │ ├── core.py │ │ │ ├── datetime.py │ │ │ ├── deferred.py │ │ │ ├── featureflags.py │ │ │ ├── flatmapped.py │ │ │ ├── functions.py │ │ │ ├── ipaddress.py │ │ │ ├── lazy.py │ │ │ ├── misc.py │ │ │ ├── numbers.py │ │ │ ├── random.py │ │ │ ├── recursive.py │ │ │ ├── regex.py │ │ │ ├── shared.py │ │ │ ├── strategies.py │ │ │ ├── strings.py │ │ │ ├── types.py │ │ │ └── utils.py │ │ ├── utils/ │ │ │ ├── __init__.py │ │ │ ├── conventions.py │ │ │ ├── deprecation.py │ │ │ ├── dynamicvariables.py │ │ │ ├── terminal.py │ │ │ └── threading.py │ │ ├── vendor/ │ │ │ ├── __init__.py │ │ │ ├── pretty.py │ │ │ └── tlds-alpha-by-domain.txt │ │ └── version.py │ ├── tests/ │ │ ├── README.md │ │ ├── __init__.py │ │ ├── array_api/ │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── conftest.py │ │ │ ├── test_argument_validation.py │ │ │ ├── test_arrays.py │ │ │ ├── test_from_dtype.py │ │ │ ├── test_indices.py │ │ │ ├── test_partial_adoptors.py │ │ │ ├── test_pretty.py │ │ │ ├── test_scalar_dtypes.py │ │ │ └── test_strategies_namespace.py │ │ ├── attrs/ │ │ │ ├── test_attrs.py │ │ │ ├── test_inference.py │ │ │ └── test_pretty.py │ │ ├── codemods/ │ │ │ ├── test_codemod_cli.py │ │ │ └── test_codemods.py │ │ ├── common/ │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── costbounds.py │ │ │ ├── debug.py │ │ │ ├── setup.py │ │ │ ├── strategies.py │ │ │ └── utils.py │ │ ├── conftest.py │ │ ├── conjecture/ │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── test_choice.py │ │ │ ├── test_choice_tree.py │ │ │ ├── test_data_tree.py │ │ │ ├── test_engine.py │ │ │ ├── test_float_encoding.py │ │ │ ├── test_forced.py │ │ │ ├── test_inquisitor.py │ │ │ ├── test_intlist.py │ │ │ ├── test_junkdrawer.py │ │ │ ├── test_local_constants.py │ │ │ ├── test_minimizer.py │ │ │ ├── test_mutations.py │ │ │ ├── test_optimiser.py │ │ │ ├── test_order_shrinking.py │ │ │ ├── test_pareto.py │ │ │ ├── test_provider.py │ │ │ ├── test_provider_contract.py │ │ │ ├── test_shrinker.py │ │ │ ├── test_shrinking_interface.py │ │ │ ├── test_test_data.py │ │ │ └── test_utils.py │ │ ├── cover/ │ │ │ ├── __init__.py │ │ │ ├── test_annotations.py │ │ │ ├── test_arbitrary_data.py │ │ │ ├── test_asyncio.py │ │ │ ├── test_cache_implementation.py │ │ │ ├── test_caching.py │ │ │ ├── test_cathetus.py │ │ │ ├── test_charmap.py │ │ │ ├── test_compat.py │ │ │ ├── test_complex_numbers.py │ │ │ ├── test_composite.py │ │ │ ├── test_composite_kwonlyargs.py │ │ │ ├── test_constants_ast.py │ │ │ ├── test_control.py │ │ │ ├── test_core.py │ │ │ ├── test_custom_reprs.py │ │ │ ├── test_database_backend.py │ │ │ ├── test_datetimes.py │ │ │ ├── test_deadline.py │ │ │ ├── test_debug_information.py │ │ │ ├── test_deferred_strategies.py │ │ │ ├── test_detection.py │ │ │ ├── test_direct_strategies.py │ │ │ ├── test_draw_example.py │ │ │ ├── test_error_in_draw.py │ │ │ ├── test_escalation.py │ │ │ ├── test_example.py │ │ │ ├── test_exceptiongroup.py │ │ │ ├── test_executors.py │ │ │ ├── test_explicit_examples.py │ │ │ ├── test_falsifying_example_output.py │ │ │ ├── test_feature_flags.py │ │ │ ├── test_filestorage.py │ │ │ ├── test_filter_rewriting.py │ │ │ ├── test_filtered_strategy.py │ │ │ ├── test_find.py │ │ │ ├── test_flakiness.py │ │ │ ├── test_float_nastiness.py │ │ │ ├── test_float_utils.py │ │ │ ├── test_functions.py │ │ │ ├── test_fuzz_one_input.py │ │ │ ├── test_given_error_conditions.py │ │ │ ├── test_health_checks.py │ │ │ ├── test_interactive_example.py │ │ │ ├── test_internal_helpers.py │ │ │ ├── test_intervalset.py │ │ │ ├── test_lambda_formatting.py │ │ │ ├── test_lazy_import.py │ │ │ ├── test_lookup.py │ │ │ ├── test_lookup_py310.py │ │ │ ├── test_lookup_py314.py │ │ │ ├── test_lookup_py37.py │ │ │ ├── test_lookup_py38.py │ │ │ ├── test_lookup_py39.py │ │ │ ├── test_map.py │ │ │ ├── test_mock.py │ │ │ ├── test_monitoring.py │ │ │ ├── test_nothing.py │ │ │ ├── test_numerics.py │ │ │ ├── test_observability.py │ │ │ ├── test_one_of.py │ │ │ ├── test_permutations.py │ │ │ ├── test_phases.py │ │ │ ├── test_posonly_args_py38.py │ │ │ ├── test_pretty.py │ │ │ ├── test_provisional_strategies.py │ │ │ ├── test_random_module.py │ │ │ ├── test_randoms.py │ │ │ ├── test_recursive.py │ │ │ ├── test_reflection.py │ │ │ ├── test_regex.py │ │ │ ├── test_regressions.py │ │ │ ├── test_replay_logic.py │ │ │ ├── test_reporting.py │ │ │ ├── test_reproduce_failure.py │ │ │ ├── test_runner_strategy.py │ │ │ ├── test_sampled_from.py │ │ │ ├── test_searchstrategy.py │ │ │ ├── test_seed_printing.py │ │ │ ├── test_settings.py │ │ │ ├── test_setup_teardown.py │ │ │ ├── test_shrink_budgeting.py │ │ │ ├── test_sideeffect_warnings.py │ │ │ ├── test_simple_characters.py │ │ │ ├── test_simple_collections.py │ │ │ ├── test_simple_strings.py │ │ │ ├── test_slices.py │ │ │ ├── test_slippage.py │ │ │ ├── test_stateful.py │ │ │ ├── test_statistical_events.py │ │ │ ├── test_subnormal_floats.py │ │ │ ├── test_targeting.py │ │ │ ├── test_testdecorators.py │ │ │ ├── test_threading.py │ │ │ ├── test_traceback_elision.py │ │ │ ├── test_type_lookup.py │ │ │ ├── test_type_lookup_forward_ref.py │ │ │ ├── test_typealias_py312.py │ │ │ ├── test_unicode_identifiers.py │ │ │ ├── test_unittest.py │ │ │ ├── test_uuids.py │ │ │ ├── test_validation.py │ │ │ └── test_verbosity.py │ │ ├── crosshair/ │ │ │ ├── test_conformance.py │ │ │ └── test_crosshair.py │ │ ├── datetime/ │ │ │ ├── __init__.py │ │ │ ├── test_dateutil_timezones.py │ │ │ ├── test_pytz_timezones.py │ │ │ └── test_zoneinfo_timezones.py │ │ ├── django/ │ │ │ ├── __init__.py │ │ │ ├── manage.py │ │ │ ├── toys/ │ │ │ │ ├── __init__.py │ │ │ │ ├── settings/ │ │ │ │ │ ├── no_urls.py │ │ │ │ │ ├── settings.py │ │ │ │ │ ├── settings_no_contrib.py │ │ │ │ │ └── urls.py │ │ │ │ └── wsgi.py │ │ │ └── toystore/ │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── forms.py │ │ │ ├── models.py │ │ │ ├── test_basic_configuration.py │ │ │ ├── test_given_forms.py │ │ │ ├── test_given_models.py │ │ │ └── views.py │ │ ├── dpcontracts/ │ │ │ ├── __init__.py │ │ │ └── test_contracts.py │ │ ├── ghostwriter/ │ │ │ ├── example_code/ │ │ │ │ ├── __init__.py │ │ │ │ └── future_annotations.py │ │ │ ├── recorded/ │ │ │ │ ├── add_custom_classes.txt │ │ │ │ ├── addition_op_magic.txt │ │ │ │ ├── addition_op_multimagic.txt │ │ │ │ ├── base64_magic.txt │ │ │ │ ├── division_binop_error_handler.txt │ │ │ │ ├── division_fuzz_error_handler.txt │ │ │ │ ├── division_operator.txt │ │ │ │ ├── division_operator_with_annotations.txt │ │ │ │ ├── division_roundtrip_arithmeticerror_handler.txt │ │ │ │ ├── division_roundtrip_error_handler.txt │ │ │ │ ├── division_roundtrip_error_handler_without_annotations.txt │ │ │ │ ├── division_roundtrip_typeerror_handler.txt │ │ │ │ ├── eval_equivalent.txt │ │ │ │ ├── fuzz_classmethod.txt │ │ │ │ ├── fuzz_sorted.txt │ │ │ │ ├── fuzz_sorted_with_annotations.txt │ │ │ │ ├── fuzz_staticmethod.txt │ │ │ │ ├── fuzz_ufunc.txt │ │ │ │ ├── fuzz_with_docstring.txt │ │ │ │ ├── hypothesis_module_magic.txt │ │ │ │ ├── invalid_types.txt │ │ │ │ ├── magic_base64_roundtrip.txt │ │ │ │ ├── magic_base64_roundtrip_with_annotations.txt │ │ │ │ ├── magic_builtins.txt │ │ │ │ ├── magic_class.txt │ │ │ │ ├── magic_gufunc.txt │ │ │ │ ├── magic_numpy.txt │ │ │ │ ├── matmul_magic.txt │ │ │ │ ├── merge_dicts.txt │ │ │ │ ├── multiplication_magic.txt │ │ │ │ ├── multiplication_operator.txt │ │ │ │ ├── multiplication_operator_unittest.txt │ │ │ │ ├── nothing_found.txt │ │ │ │ ├── optional_parameter.txt │ │ │ │ ├── optional_union_parameter.txt │ │ │ │ ├── re_compile.txt │ │ │ │ ├── re_compile_except.txt │ │ │ │ ├── re_compile_unittest.txt │ │ │ │ ├── sequence_from_collections.txt │ │ │ │ ├── sorted_idempotent.txt │ │ │ │ ├── sorted_self_equivalent.txt │ │ │ │ ├── sorted_self_equivalent_with_annotations.txt │ │ │ │ ├── sorted_self_error_equivalent_1error.txt │ │ │ │ ├── sorted_self_error_equivalent_2error_unittest.txt │ │ │ │ ├── sorted_self_error_equivalent_simple.txt │ │ │ │ ├── sorted_self_error_equivalent_threefuncs.txt │ │ │ │ ├── timsort_idempotent.txt │ │ │ │ ├── timsort_idempotent_asserts.txt │ │ │ │ └── union_sequence_parameter.txt │ │ │ ├── test_expected_output.py │ │ │ ├── test_ghostwriter.py │ │ │ ├── test_ghostwriter_cli.py │ │ │ └── try-writing-for-installed.py │ │ ├── lark/ │ │ │ ├── __init__.py │ │ │ └── test_grammar.py │ │ ├── nocover/ │ │ │ ├── __init__.py │ │ │ ├── test_argument_validation.py │ │ │ ├── test_bad_repr.py │ │ │ ├── test_baseexception.py │ │ │ ├── test_boundary_exploration.py │ │ │ ├── test_build_signature.py │ │ │ ├── test_cache_implementation.py │ │ │ ├── test_cacheable.py │ │ │ ├── test_characters.py │ │ │ ├── test_collective_minimization.py │ │ │ ├── test_compat.py │ │ │ ├── test_completion.py │ │ │ ├── test_complex_numbers.py │ │ │ ├── test_conjecture_engine.py │ │ │ ├── test_conjecture_int_list.py │ │ │ ├── test_conjecture_utils.py │ │ │ ├── test_conventions.py │ │ │ ├── test_database_agreement.py │ │ │ ├── test_database_usage.py │ │ │ ├── test_deferred_errors.py │ │ │ ├── test_drypython_returns.py │ │ │ ├── test_duplication.py │ │ │ ├── test_dynamic_variable.py │ │ │ ├── test_emails.py │ │ │ ├── test_eval_as_source.py │ │ │ ├── test_exceptiongroup.py │ │ │ ├── test_explore_arbitrary_languages.py │ │ │ ├── test_fancy_repr.py │ │ │ ├── test_filtering.py │ │ │ ├── test_find.py │ │ │ ├── test_fixtures.py │ │ │ ├── test_flatmap.py │ │ │ ├── test_floating.py │ │ │ ├── test_from_type_recipe.py │ │ │ ├── test_given_error_conditions.py │ │ │ ├── test_given_reuse.py │ │ │ ├── test_health_checks.py │ │ │ ├── test_imports.py │ │ │ ├── test_integer_ranges.py │ │ │ ├── test_interesting_origin.py │ │ │ ├── test_labels.py │ │ │ ├── test_large_examples.py │ │ │ ├── test_limits.py │ │ │ ├── test_modify_inner_test.py │ │ │ ├── test_nesting.py │ │ │ ├── test_precise_shrinking.py │ │ │ ├── test_pretty_repr.py │ │ │ ├── test_randomization.py │ │ │ ├── test_recursive.py │ │ │ ├── test_regex.py │ │ │ ├── test_regressions.py │ │ │ ├── test_reusable_values.py │ │ │ ├── test_sampled_from.py │ │ │ ├── test_scrutineer.py │ │ │ ├── test_sets.py │ │ │ ├── test_sharing.py │ │ │ ├── test_simple_numbers.py │ │ │ ├── test_simple_strings.py │ │ │ ├── test_skipping.py │ │ │ ├── test_stateful.py │ │ │ ├── test_strategy_state.py │ │ │ ├── test_subnormal_floats.py │ │ │ ├── test_targeting.py │ │ │ ├── test_testdecorators.py │ │ │ ├── test_threading.py │ │ │ ├── test_type_lookup.py │ │ │ ├── test_type_lookup_forward_ref.py │ │ │ ├── test_type_lookup_future_annotations.py │ │ │ ├── test_unusual_settings_configs.py │ │ │ └── test_uuids.py │ │ ├── numpy/ │ │ │ ├── __init__.py │ │ │ ├── test_argument_validation.py │ │ │ ├── test_deprecation.py │ │ │ ├── test_fill_values.py │ │ │ ├── test_floor_ceil.py │ │ │ ├── test_from_dtype.py │ │ │ ├── test_from_type.py │ │ │ ├── test_gen_data.py │ │ │ ├── test_gufunc.py │ │ │ ├── test_import.py │ │ │ ├── test_narrow_floats.py │ │ │ ├── test_randomness.py │ │ │ └── test_sampled_from.py │ │ ├── pandas/ │ │ │ ├── __init__.py │ │ │ ├── helpers.py │ │ │ ├── test_argument_validation.py │ │ │ ├── test_data_frame.py │ │ │ ├── test_indexes.py │ │ │ └── test_series.py │ │ ├── patching/ │ │ │ ├── __init__.py │ │ │ ├── callables.py │ │ │ ├── test_patching.py │ │ │ └── toplevel.py │ │ ├── pytest/ │ │ │ ├── test__pytest.py │ │ │ ├── test_capture.py │ │ │ ├── test_checks.py │ │ │ ├── test_collection_warning.py │ │ │ ├── test_compat.py │ │ │ ├── test_constant_collection_timing.py │ │ │ ├── test_doctest.py │ │ │ ├── test_fixtures.py │ │ │ ├── test_junit.py │ │ │ ├── test_mark.py │ │ │ ├── test_parametrized_db_keys.py │ │ │ ├── test_profiles.py │ │ │ ├── test_pytest_detection.py │ │ │ ├── test_reporting.py │ │ │ ├── test_runs.py │ │ │ ├── test_seeding.py │ │ │ ├── test_sideeffect_warnings.py │ │ │ ├── test_skipping.py │ │ │ └── test_statistics.py │ │ ├── quality/ │ │ │ ├── __init__.py │ │ │ ├── test_deferred_strategies.py │ │ │ ├── test_discovery_ability.py │ │ │ ├── test_float_shrinking.py │ │ │ ├── test_integers.py │ │ │ ├── test_poisoned_lists.py │ │ │ ├── test_poisoned_trees.py │ │ │ ├── test_shrink_quality.py │ │ │ └── test_zig_zagging.py │ │ ├── redis/ │ │ │ ├── __init__.py │ │ │ └── test_redis_exampledatabase.py │ │ ├── test_annotated_types.py │ │ ├── typing_extensions/ │ │ │ ├── __init__.py │ │ │ └── test_backported_types.py │ │ └── watchdog/ │ │ ├── __init__.py │ │ ├── test_database.py │ │ └── test_database_cover.py │ └── tox.ini ├── notebooks/ │ └── Designing a better simplifier.ipynb ├── paper.bib ├── paper.md ├── pyproject.toml ├── requirements/ │ ├── coverage.in │ ├── coverage.txt │ ├── crosshair.in │ ├── crosshair.txt │ ├── fuzzing.in │ ├── fuzzing.txt │ ├── test.in │ ├── test.txt │ ├── tools.in │ └── tools.txt ├── tooling/ │ ├── README.md │ ├── codespell-dict.txt │ ├── codespell-ignore.txt │ ├── scripts/ │ │ ├── common.sh │ │ ├── ensure-python.sh │ │ └── tool-hash.py │ ├── setup.py │ └── src/ │ └── hypothesistooling/ │ ├── __init__.py │ ├── __main__.py │ ├── installers.py │ ├── junkdrawer.py │ ├── projects/ │ │ ├── __init__.py │ │ └── hypothesispython.py │ ├── releasemanagement.py │ └── scripts.py ├── website/ │ ├── archive-redirect.html │ ├── content/ │ │ ├── 2016-04-15-economics-of-software-correctness.md │ │ ├── 2016-04-15-getting-started-with-hypothesis.md │ │ ├── 2016-04-16-anatomy-of-a-test.md │ │ ├── 2016-04-16-encode-decode-invariant.md │ │ ├── 2016-04-16-quickcheck-in-every-language.md │ │ ├── 2016-04-16-the-purpose-of-hypothesis.md │ │ ├── 2016-04-19-rule-based-stateful-testing.md │ │ ├── 2016-04-29-testing-performance-optimizations.md │ │ ├── 2016-05-02-referential-transparency.md │ │ ├── 2016-05-11-generating-the-right-data.md │ │ ├── 2016-05-13-what-is-property-based-testing.md │ │ ├── 2016-05-26-exploring-voting-with-hypothesis.md │ │ ├── 2016-05-29-testing-optimizers-with-hypothesis.md │ │ ├── 2016-05-31-looking-for-guest-posts.md │ │ ├── 2016-06-05-incremental-property-based-testing.md │ │ ├── 2016-06-13-testing-configuration-parameters.md │ │ ├── 2016-06-30-tests-as-complete-specifications.md │ │ ├── 2016-07-04-calculating-the-mean.md │ │ ├── 2016-07-09-hypothesis-3.4.1-release.md │ │ ├── 2016-07-13-hypothesis-3.4.2-release.md │ │ ├── 2016-07-23-what-is-hypothesis.md │ │ ├── 2016-08-09-hypothesis-pytest-fixtures.md │ │ ├── 2016-08-19-recursive-data.md │ │ ├── 2016-08-31-how-many-tests.md │ │ ├── 2016-09-04-hypothesis-vs-eris.md │ │ ├── 2016-09-23-hypothesis-3.5.0-release.md │ │ ├── 2016-10-01-pytest-integration-sponsorship.md │ │ ├── 2016-10-17-canonical-serialization.md │ │ ├── 2016-10-31-hypothesis-3.6.0-release.md │ │ ├── 2016-12-05-integrated-shrinking.md │ │ ├── 2016-12-08-compositional-shrinking.md │ │ ├── 2016-12-10-how-hypothesis-works.md │ │ ├── 2017-03-09-hypothesis-for-researchers.md │ │ ├── 2017-04-05-how-not-to-die-hard-with-hypothesis.md │ │ ├── 2017-07-16-types-and-properties.md │ │ ├── 2017-09-14-multi-bug-discovery.md │ │ ├── 2017-09-28-threshold-problem.md │ │ ├── 2018-01-08-smarkets.md │ │ ├── 2018-02-27-continuous-releases.md │ │ ├── 2020-06-08-complex-data-strategies.md │ │ ├── 2025-08-07-thread-safe.md │ │ ├── 2025-11-01-claude-code-plugin.md │ │ ├── 2025-11-16-introducing-hypofuzz.md │ │ └── pages/ │ │ └── testimonials.md │ ├── pelicanconf.py │ └── theme/ │ ├── static/ │ │ ├── prism.css │ │ ├── prism.js │ │ └── style.css │ └── templates/ │ ├── article-card.html │ ├── article.html │ ├── base.html │ ├── category.html │ ├── index.html │ └── page.html └── whole_repo_tests/ ├── __init__.py ├── documentation/ │ ├── __init__.py │ └── test_documentation.py ├── types/ │ ├── __init__.py │ ├── revealed_types.py │ ├── test_hypothesis.py │ ├── test_mypy.py │ └── test_pyright.py └── whole_repo/ ├── __init__.py ├── test_ci_config.py ├── test_deploy.py ├── test_release_files.py ├── test_release_management.py ├── test_requirements.py ├── test_rst_is_valid.py ├── test_shellcheck.py └── test_validate_branch_check.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/CLAUDE.md ================================================ # Hypothesis Development Guide ## Essential Reading **Always read `CONTRIBUTING.rst` before starting work**, especially before writing tests or creating a PR. ## Testing ### Running Tests Run tests using the build system: - **Quick test run**: `./build.sh check-coverage` (curated subset with coverage verification) - **Python version-specific**: `./build.sh check-py311` (replace with target version) - **Fine-grained control**: `./build.sh tox py311-custom 3.11.3 -- [pytest args]` - **Direct pytest** (after setup): `pytest hypothesis-python/tests/cover/` ### Writing Tests **Never use `.example()` method in tests.** Instead: - Use `@given` decorator directly for property-based tests - Use helper functions from `tests.common.debug`: - `minimal()` - find minimal failing example - `find_any()` - find any example matching condition - `assert_all_examples()` - verify all examples match predicate - `assert_simple_property()` - verify simple properties with few examples - `check_can_generate_examples()` - verify strategy can generate without error ## Changelog & Pull Requests When creating a PR that changes `hypothesis-python/src/`: 1. Create `hypothesis-python/RELEASE.rst` with `RELEASE_TYPE: patch` (bugfixes) or `minor` (features) 2. See `RELEASE-sample.rst` for examples 3. **Imitate the style in `changelog.rst`** for consistency 4. Follow all changelog instructions in `CONTRIBUTING.rst` **Note:** Test-only changes (no modifications to `src/`) do not require a RELEASE.rst file. ## Before Committing 1. Do a final edit pass on all code to ensure it is: - **Concise** - remove unnecessary verbosity - **Idiomatic** - follows Python and Hypothesis conventions - **Minimally commented** - code should be self-documenting; only add comments where truly needed 2. **Run `./build.sh format; ./build.sh lint`** immediately before committing to auto-format and lint code 3. **Do not reference issues or PRs in commit messages** (e.g., avoid `Fixes #1234` or `See #5678`) - this clutters the issue timeline with unnecessary links ================================================ FILE: .claude/commands/hypothesis.md ================================================ --- description: Write property-based tests with Hypothesis --- You are an expert developer of property-based tests, specifically using Hypothesis. Your goal is to identify and implement a small number of the most valuable Hypothesis tests that would benefit an existing codebase right now. You focus on clarity and maintainability, as your code will be reviewed by a developer. Your goal is to write precise tests, not comprehensive test suites. Create and follow this todo list using the `Todo` tool: 1. [ ] Explore the provided code and identify valuable properties. 2. [ ] For each property, explore how its related code is used. 3. [ ] Write Hypothesis tests based on those properties. 4. [ ] Run the new Hypothesis tests, and reflect on the result. ## 1. Explore the code provided and identify valuable properties First, explore the provided code, and identify valuable properties to test. A "valuable property" is an invariant or property about the code that is valuable to the codebase right now and that a knowledgeable developer for this codebase would have written a Hypothesis test for. The following are indicative of a valuable property: - Would catch important bugs: Testing this property would reveal bugs that could cause serious issues. - Documents important behavior: The property captures essential assumptions or guarantees that are important to future or current developers. - Benefits significantly from Hypothesis: The property is concisely and powerfully expressed as a Hypothesis test, rather than a series of unit tests. Keep the following in mind: - Only identify properties that you strongly believe to be true and that are supported by evidence in the codebase, for example in docstrings, comments, code use patterns, type hints, etc. Do not include properties you are at all unsure about. - Each property should provide a substantial improvement in testing power or clarity when expressed as a Hypothesis test, rather than a unit test. Properties which could have been equally well tested with a unit test are not particularly valuable. - You may come across many possible properties. Your goal is to identify only a small number of the most valuable of those properties that would benefit the codebase right now. If the provided code is large, focus on exploring in this order: 1. Public API functions/classes 2. Well-documented implementations of core functionality 3. Other implementations of core functionality 4. Internal/private helpers or utilities Here are some examples of typical properties: - Round-trip property: `decode(encode(x)) = x`, `parse(format(x)) = x`. - Inverse relationship: `add/remove`, `push/pop`, `create/destroy`. - Multiple equivalent implementations: Optimized vs reference implementation, complicated vs simple implementation. - Mathematical property: Idempotence `f(f(x)) = f(x)`, commutativity `f(x, y) = f(y, x)`. - Invariants: `len(filter(x)) <= len(x)`, `set(sort(x)) == set(x)`. - Confluence: the order of function application doesn't matter (for example, in compiler optimization passes). - Metamorphic property: some relationship between `f(x)` and `g(x)` holds for all x. For example, `sin(π − x) = sin(x)`. - Single entry point. If a library has a narrow public API, a nice property-based test simply calls the library with valid inputs. Common in parsers. While the following should generally not be tested: - Obvious code wrappers - Implementation details The user has provided the following guidance for where and how to add Hypothesis tests: $ARGUMENTS. - If the user has provided no direction, explore the entire codebase. - If the user has provided a specific module, explore that module. - If the user has provided a specific file, explore that file. - If the user has provided a specific function, explore that function. - If the user has given more complex guidance, follow that instead. If you don't identify any valuable properties during exploration, that's fine; just tell the user as much, and then stop. At the end of this step, you should tell the user the small list of the most valuable properties that you intend to test. ## 2. For each valuable property, explore how its related code is used Before writing Hypothesis tests, explore how the codebase uses the related code of each valuable property. For example, if a property involves a function `some_function`, explore how the codebase calls `some_function`: what kinds of inputs are passed to it? in what context? etc. This helps correct any misunderstanding about the property before writing a test for it. ## 3. Write Hypothesis tests based on those properties. For each property, write a new Hypothesis test for it, and add it to the codebase's test suite, following its existing testing conventions. When writing Hypothesis tests, follow these guidelines: - Each Hypothesis test should be both sound (tests only inputs the code can actually be called with) and complete (tests all inputs the code can actually be called with). Sometimes this is difficult. In those cases, prefer sound and mostly-complete tests; stopping at 90% completeness is better than over-complicating a test. - Only place constraints on Hypothesis strategies if required by the code. For example, prefer `st.lists(...)` (with no size bound) to `st.lists(..., max_size=100)`, unless the property explicitly happens to only be valid for lists with no more than 100 elements. ## 4. Run the new Hypothesis tests, and reflect on the result. Run the new Hypothesis tests that you just added. If any fail, reflect on why. Is the test failing because of a genuine bug, or because it's not testing the right thing? Often, when a new Hypothesis test fails, it's because the test generates inputs that the codebase assumes will never occur. If necessary, re-explore related parts of the codebase to check your understanding. You should only report that the codebase has a bug to the user if you are truly confident, and can justify why. # Hypothesis Reference Documentation reference (fetch with the `WebFetch` tool if required): - **Strategies API reference**: https://hypothesis.readthedocs.io/en/latest/reference/strategies.html - **Other API reference**: https://hypothesis.readthedocs.io/en/latest/reference/api.html - Documents `@settings`, `@given`, etc. These Hypothesis strategies are under-appreciated for how effective they are. Use them if they are a perfect or near-perfect fit for a property: - `st.from_regex` - `st.from_lark` - for context-free grammars - `st.functions` - generates arbitrary callable functions ================================================ FILE: .gitattributes ================================================ * text eol=lf # Denote all files that are truly binary and should not be modified. *.png binary *.jpg binary *.gif binary ================================================ FILE: .github/CODEOWNERS ================================================ # Engine changes need to be approved by Zac-HD, as per # https://github.com/HypothesisWorks/hypothesis/blob/master/guides/review.rst#engine-changes /hypothesis-python/src/hypothesis/internal/conjecture/ @Zac-HD @Liam-DeVoe # Changes to the paper also need to be approved by DRMacIver or Zac, as authors /paper.md @DRMacIver @Zac-HD /paper.bib @DRMacIver @Zac-HD ================================================ FILE: .github/CODE_OF_CONDUCT.rst ================================================ --------------- Code of conduct --------------- Hypothesis's community is an inclusive space, and everyone in it is expected to abide by a code of conduct. This applies in issues, pull requests, etc. as well as in the various Hypothesis community spaces. At the high level the code of conduct goes like this: 1. Be kind 2. Be respectful 3. Be helpful While it is impossible to enumerate everything that is unkind, disrespectful or unhelpful, here are some specific things that are definitely against the code of conduct: 1. -isms and -phobias (e.g. racism, sexism, transphobia and homophobia) are unkind, disrespectful *and* unhelpful. Just don't. 2. All software is broken. This is not a moral failing on the part of the authors. Don't give people a hard time for bad code. 3. It's OK not to know things. Everybody was a beginner once, nobody should be made to feel bad for it. 4. It's OK not to *want* to know something. If you think someone's question is fundamentally flawed, you should still ask permission before explaining what they should actually be asking. 5. Note that "I was just joking" is not a valid defence. 6. Don't suggest violence as a response to things, e.g. "People who do/think X should be Y-ed". Even if you think it is obvious hyperbole and that it's very clear that no actual threat is meant, it still contributes to a culture that makes people feel unsafe. ~~~~~~~~~~~~~~~~~~~~~~~~ Resolution of Violations ~~~~~~~~~~~~~~~~~~~~~~~~ David R. MacIver (the project lead) acts as the main point of contact and enforcer for code of conduct violations. You can email him at david@drmaciver.com, or for violations on GitHub that you want to draw his attention to you can also mention him as @DRMacIver. Other people (especially Hypothesis team members) should feel free to call people on code of conduct violations when they see them, and it is appreciated but not required (especially if doing so would make you feel uncomfortable or unsafe). We don't currently have a formal policy for resolutions and it's mostly based on subjective judgement calls, but the high level intent is as follows: * minor one-off infractions will just be met with a request not to repeat the behaviour and, where it would be useful, for an apology. * Major infractions and repeat offenders will be banned from the community. If you disagree with David's judgement on any particular event, please feel free to tell him so. Also, people who have a track record of bad behaviour outside of the Hypothesis community may be banned even if they obey all these rules if their presence is making people uncomfortable. ================================================ FILE: .github/actions/install-base/action.yml ================================================ name: "Install" description: "Install python, then cache" inputs: python-version: description: "Python version" required: true python-architecture: description: "Python architecture, if not default for platform" task: description: "Task name" required: true runs: using: "composite" steps: - name: Set up Python ${{ inputs.python-version }} ${{ inputs.python-architecture }} uses: actions/setup-python@v4 with: python-version: ${{ inputs.python-version }} architecture: ${{ inputs.python-architecture }} - name: Install dotnet6 for Pyjion if: ${{ endsWith(inputs.task, '-pyjion') }} shell: bash run: | wget https://packages.microsoft.com/config/ubuntu/21.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb rm packages-microsoft-prod.deb sudo apt-get update sudo apt-get install -y apt-transport-https && \ sudo apt-get update && \ sudo apt-get install -y dotnet-sdk-6.0 - name: Restore cache uses: actions/cache@v3 with: path: | .tox/ vendor/bundle ~/.cache ~/.local ~/appdata/local/pip/cache ~/Library/Caches/pip ~/wheelhouse key: deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }}-${{ inputs.python-version }}-${{ inputs.python-architecture }}-${{ inputs.task }} restore-keys: | deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }}-${{ inputs.python-version }}-${{ inputs.python-architecture }} deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }} deps-${{ runner.os }} ================================================ FILE: .github/workflows/fuzz.yml ================================================ name: Fuzzing env: # Tell pytest and other tools to produce coloured terminal output. # Make sure this is also in the "passenv" section of the tox config. PY_COLORS: 1 on: # Run every six hours, for six hours each time schedule: - cron: '0 */6 * * *' # Allow manual launching too so we can test any branch we like workflow_dispatch: # # Enable this and reduce the timeout below to check a PR is working # pull_request: # branches: [ master ] jobs: fuzz: if: github.repository == 'HypothesisWorks/hypothesis' || github.event_name == 'workflow_dispatch' # Keep all of this stuff synced with the setup in main.yml for CI runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python 3.14 uses: actions/setup-python@v4 with: python-version: "3.14.2" - name: Restore cache uses: actions/cache@v3 with: path: | ~/.cache ~/wheelhouse ~/.local vendor/bundle .tox/ key: deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }}-${{ matrix.task }} restore-keys: | deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }} deps-${{ runner.os }} # OK, on to the fuzzing-specific part. # We're going to stick everything into a single run for now instead of # sharding it, because we'd have to manually specify all the databases # we want to multiplex across and that would be annoying to manage. # TODO: revisit this later; a redis-like service would be so much nicer. - name: Download example database uses: dawidd6/action-download-artifact@v9 with: name: hypothesis-example-db path: .hypothesis/examples if_no_artifact_found: warn workflow_conclusion: completed - name: Install dependencies run: | pip install --upgrade setuptools pip wheel pip install -r requirements/fuzzing.txt pip install hypothesis-python/[all] - name: Run hypofuzz session continue-on-error: true # The timeout ensures that we finish all steps within the six-hour # maximum runtime for Github Actions. # Then run the fuzzer on everything, as for our Windows CI; avoiding # the --no-dashboard option because that also disables .patch writing. run: | timeout --preserve-status 5.5h \ hypothesis fuzz -- hypothesis-python/tests/ \ --ignore=hypothesis-python/tests/quality/ \ --ignore=hypothesis-python/tests/ghostwriter/ - name: Upload patch files with covering and failing `@example()`s uses: actions/upload-artifact@v4 if: always() with: name: explicit-example-patches path: .hypothesis/patches/latest_hypofuzz_*.patch # Upload the database so it'll be persisted between runs. # Note that we can also pull it down to use locally via # https://hypothesis.readthedocs.io/en/latest/database.html#hypothesis.database.GitHubArtifactDatabase - name: Upload example database uses: actions/upload-artifact@v4 if: always() with: name: hypothesis-example-db path: .hypothesis/examples ================================================ FILE: .github/workflows/main.yml ================================================ name: Hypothesis CI env: # Tell pytest and other tools to produce coloured terminal output. # Make sure this is also in the "passenv" section of the tox config. PY_COLORS: 1 on: push: branches: [ master ] pull_request: branches: [ master ] workflow_dispatch: # Cancel in-progress PR builds if another commit is pushed. # On non-PR builds, fall back to the globally-unique run_id and don't cancel. concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: basic: runs-on: ubuntu-latest strategy: matrix: task: - tox-cover - tox-nocover - tox-rest - check-whole-repo-tests - check-types-hypothesis - lint - check-format fail-fast: false env: PYTHON_VERSION: "3.14" steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: "Install base for Python ${{ env.PYTHON_VERSION }}" uses: ./.github/actions/install-base with: python-version: ${{ env.PYTHON_VERSION }} task: ${{ matrix.task }} - name: Install tox if: ${{ startsWith(matrix.task, 'tox-') }} run: | pip install --upgrade setuptools pip wheel pip install tox - name: Run tests run: | export TASK=${{ matrix.task }} if [[ $TASK == tox-* ]]; then TOX_TASK="${TASK#tox-}" cd hypothesis-python tox -e $TOX_TASK else ./build.sh fi req: needs: [basic] runs-on: ubuntu-latest strategy: matrix: task: - check-documentation - check-types-api - check-coverage - check-conjecture-coverage - check-py310-cover - check-py310-nocover - check-py310-niche - check-pypy310-cover # - check-py310-pyjion # see notes in tox.ini - check-py311-cover - check-py311-nocover - check-py311-niche - check-pypy311-cover - check-py312-cover - check-py312-nocover - check-py312-niche - check-py313-cover - check-py313-nocover - check-py313-niche - check-py313t-cover - check-py313t-nocover - check-py313t-niche - check-quality - check-pytest9 - check-pytest84 - check-pytest74 - check-py311-pytest62 - check-django60 - check-django42 - check-py313-pandas22 - check-py312-pandas21 - check-py311-pandas20 - check-py311-pandas15 - check-py310-pandas14 - check-py310-pandas13 ## FIXME: actions update means Python builds without eg _bz2, which was required # - check-py310-pandas12 # - check-py310-pandas11 fail-fast: false env: PYTHON_VERSION: "3.14" steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: "Install base for Python ${{ env.PYTHON_VERSION }}" uses: ./.github/actions/install-base with: python-version: ${{ env.PYTHON_VERSION }} task: ${{ matrix.task }} - name: Run tests run: | export TASK=${{ matrix.task }} if [[ $TASK == check-crosshair-custom-* ]]; then GROUP="${TASK#check-crosshair-custom-}" ./build.sh check-crosshair-custom -- -n auto $(cd hypothesis-python && echo tests/$GROUP | xargs -n1 echo | grep -Ev "_py312|_py314" | xargs) else ./build.sh fi - name: Upload coverage data uses: actions/upload-artifact@v4 # Invoke the magic `always` function to run on both success and failure. if: ${{ always() && endsWith(matrix.task, '-coverage') }} with: name: ${{ matrix.task }}-data include-hidden-files: true path: | hypothesis-python/.coverage* !hypothesis-python/.coveragerc hypothesis-python/branch-check* nonreq: needs: [basic] runs-on: ubuntu-latest strategy: matrix: task: - check-py314-cover - check-py314-nocover # Blocked by a 3.14rc1 bug. see # https://github.com/HypothesisWorks/hypothesis/pull/4490#issuecomment-3144989862. # Can revisit in 3.14rc2. # - check-py314-niche - check-py314t-cover - check-py314t-nocover - check-py314t-niche # - check-py315-cover # - check-py315-nocover # - check-py315-niche # - check-py315t-cover # - check-py315t-nocover # - check-py315t-niche - check-django52 ## `-cover` is too slow under crosshair; use a custom split - check-crosshair-custom-cover/test_[a-d]* - check-crosshair-custom-cover/test_[e-i]* - check-crosshair-custom-cover/test_[j-r]* - check-crosshair-custom-cover/test_[s-z]* - check-crosshair-custom-pytest/test_* - check-crosshair-custom-nocover/test_[a-d]* - check-crosshair-custom-nocover/test_[e-i]* - check-crosshair-custom-nocover/test_[j-r]* - check-crosshair-custom-nocover/test_[s-z]* # - check-crosshair-niche - check-threading - check-py310-oldestnumpy - check-numpy-nightly fail-fast: false env: PYTHON_VERSION: "3.14" steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: "Install base for Python ${{ env.PYTHON_VERSION }}" uses: ./.github/actions/install-base with: python-version: ${{ env.PYTHON_VERSION }} task: ${{ matrix.task }} - name: Run tests run: | export TASK=${{ matrix.task }} if [[ $TASK == check-crosshair-custom-* ]]; then GROUP="${TASK#check-crosshair-custom-}" ./build.sh check-crosshair-custom -- -n auto $(cd hypothesis-python && echo tests/$GROUP | xargs -n1 echo | grep -Ev "_py312|_py314" | xargs) else ./build.sh fi cross: needs: [basic] strategy: matrix: os: - windows-latest - macos-latest python-version: - "3.11" - "3.14" python-architecture: - null - "x86" task: - cover - nocover - rest - alt-nocover - alt-rest exclude: - { os: macos-latest, python-architecture: "x86" } - { python-version: "3.14", python-architecture: "x86" } - { python-version: "3.11", task: nocover } - { python-version: "3.11", task: rest } - { python-version: "3.14", task: alt-nocover } - { python-version: "3.14", task: alt-rest } fail-fast: false runs-on: ${{ matrix.os }} env: # Override default from tox.ini PYTHONWARNDEFAULTENCODING: "" steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: "Install base for Python ${{ matrix.python-version }} ${{ matrix.python-architecture }}" uses: ./.github/actions/install-base with: python-version: ${{ matrix.python-version }} python-architecture: ${{ matrix.python-architecture }} task: ${{ matrix.task }} - name: Install tox run: | pip install --upgrade setuptools pip wheel pip install tox - name: Run tests working-directory: ./hypothesis-python run: | tox -e ${{ matrix.task }} # See https://pyodide.org/en/stable/usage/building-and-testing-packages.html # and https://github.com/numpy/numpy/blob/9a650391651c8486d8cb8b27b0e75aed5d36033e/.github/workflows/emscripten.yml test-pyodide: needs: [basic] runs-on: ubuntu-latest env: NODE_VERSION: 22 # Note that the versions below are updated by `update_pyodide_versions()` in our weekly cronjob. # The versions of pyodide-build and the Pyodide runtime may differ. PYODIDE_VERSION: 0.29.3 PYODIDE_BUILD_VERSION: 0.33.0 # pyodide 0.29.0 (latest at time of writing) doesn't yet support 3.14 PYTHON_VERSION: 3.13.2 steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: "Install base for Python ${{ env.PYTHON_VERSION }}" uses: ./.github/actions/install-base with: python-version: ${{ env.PYTHON_VERSION }} task: pyodide - name: Set up Node uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version: ${{ env.NODE_VERSION }} - name: Install pyodide-build and Pyodide cross-build environment run: | pip install pyodide-build==${{ env.PYODIDE_BUILD_VERSION }} pyodide xbuildenv install ${{ env.PYODIDE_VERSION }} - name: Set up Pyodide venv and install dependencies run: | pip install --upgrade setuptools pip wheel build python -m build --wheel hypothesis-python --outdir dist/ pip download --dest=dist/ hypothesis-python/ pytest tzdata # fetch all the wheels rm dist/packaging-*.whl # fails with `invalid metadata entry 'name'` pyodide venv .venv-pyodide source .venv-pyodide/bin/activate pip install dist/*.whl - name: Run tests run: | source .venv-pyodide/bin/activate # pyodide can't run multiple processes internally, so parallelize explicitly over # discovered test files instead (20 at a time) TEST_FILES=$(ls hypothesis-python/tests/cover/test*.py | grep -v "_py314") echo "test files: $TEST_FILES" parallel --max-procs 100% --max-args 20 --keep-order --line-buffer \ python -m pytest -p no:cacheprovider <<< $TEST_FILES check-required: if: always() needs: [basic, req, cross] runs-on: ubuntu-latest steps: - name: Check required jobs uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} deploy: if: "github.event_name == 'push' && github.repository == 'HypothesisWorks/hypothesis'" runs-on: ubuntu-latest needs: [check-required] strategy: matrix: task: - deploy fail-fast: false steps: - uses: actions/checkout@v3 with: fetch-depth: 0 token: ${{ secrets.GH_TOKEN }} - uses: ./.github/actions/install-base with: python-version: "3.14" - name: Deploy package env: GH_TOKEN: ${{ secrets.GH_TOKEN }} TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | TASK=${{ matrix.task }} ./build.sh ================================================ FILE: .github/workflows/update-deps.yml ================================================ name: Update pinned dependencies on: schedule: - cron: 0 0 * * 0 workflow_dispatch: jobs: release: name: Update pinned dependencies runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.14 uses: actions/setup-python@v4 with: python-version: '3.14' - name: Update pinned dependencies run: ./build.sh upgrade-requirements - name: Open pull request uses: peter-evans/create-pull-request@v7 with: token: ${{secrets.GH_TOKEN}} delete-branch: true title: Update pinned dependencies body: | Automatically update pinned dependencies commit-message: 'Update pinned dependencies' committer: 'CI on behalf of the Hypothesis team ' author: 'CI on behalf of the Hypothesis team ' ================================================ FILE: .github/workflows/website.yml ================================================ name: Build website & deploy to GitHub Pages on: # Runs on pushes targeting the default branch push: branches: ["master"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: "pages" cancel-in-progress: false jobs: # Single deploy job since we're just deploying deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Build website run: ./build.sh website - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: 'website/output/' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .gitignore ================================================ # mypyc *.so # misc (editors, file systems, etc) *.swo *.swp .idea .vagrant .DS_Store .hypothesis .vscode/ .claude/settings.local.json # generic build components .runtimes /hypothesis-python/branch-check* /pythonpython3.* /pythonpypy3.* .pyodide-xbuildenv # python *.pyc *.pyo venv* .cache .pytest_cache .mypy_cache docs/_build *.egg-info _build .tox .coverage .coverage.* .pypirc htmlcov build dist .doctrees/ .v*/ # encrypted files secrets.tar secrets _site/ .sass-cache/ .docker # ========================= # Operating System Files # ========================= # OSX # ========================= .AppleDouble .LSOverride # Thumbnails ._* # Files that might appear on external disk .Spotlight-V100 .Trashes # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Windows # ========================= # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk sftp-config.json # Vim files *.sw* __pycache__ .jekyll-metadata HypothesisWorks.github.io.iml jekyll.log /website/output/ /t.py ================================================ FILE: .readthedocs.yml ================================================ # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Optionally build your docs in additional formats such as PDF and ePub formats: - htmlzip - epub # - pdf # busted by latex crash on unicode U+030A combining ring above, in text() docs # Optionally set the version of Python and requirements required to build your docs build: os: ubuntu-22.04 tools: python: "3.14" python: install: - requirements: requirements/tools.txt - path: hypothesis-python/ extra_requirements: - all sphinx: configuration: hypothesis-python/docs/conf.py ================================================ FILE: AUTHORS.rst ================================================ -------------------- List of Contributors -------------------- The primary author for most of Hypothesis is David R. MacIver (me). However the following people have also contributed work. As well as my thanks, they also have copyright over their individual contributions. .. NOTE - this list is in alphabetical order by first name (or handle). * `A. Jesse Jiryu Davis `_ * `Aaron Meurer `_ * `Adam Johnson `_ * `Adam Matan _` * `Adam Sven Johnson `_ * `Afrida Tabassum `_ (afrida@gmail.com) * `Afonso Silva `_ (ajcerejeira@gmail.com) * `Agriya Khetarpal `_ * `Agustín Covarrubias `_ (gh@agucova.dev) * `Akash Suresh `_ (akashsuresh36@gmail.com) * `Alex Gaynor `_ * `Alex Stapleton `_ * `Alex Willmer `_ (alex@moreati.org.uk) * `Andrea Pierré `_ * `Andrea Reina `_ * `Andrew Sansom `_ * `Anne Archibald `_ * `Arjoonn Sharma `_ * `Ben Anhalt `_ * `Ben Peterson `_ (killthrush@hotmail.com) * `Benjamin Lee `_ (benjamindlee@me.com) * `Benjamin Palmer `_ * `Bex Dunn `_ (bex.dunn@gmail.com) * `Bill Tucker `_ (imbilltucker@gmail.com) * `Brandon Chinn `_ * `Bryant Eisenbach `_ * `Buck Evan, copyright Google LLC `_ * `Cameron McGill `_ * `Carl Meyer `_ * `Charles O'Farrell `_ * `Charlie Tanksley `_ * `Chase Garner `_ (chase@garner.red) * `Cheuk Ting Ho `_ * `Chris Down `_ * `Chris van Dronkelaar `_ * `Chris Wesseling `_ * `Christopher Martin `_ (ch.martin@gmail.com) * `Claudio Jolowicz `_ * `Conrad Ho `_ (conrad.alwin.ho@gmail.com) * `Cory Benfield `_ * `Cristi Cobzarenco `_ (cristi@reinfer.io) * `Damon Francisco `_ (damontfrancisco@yahoo.com) * `Daniel J. West `_ * `Daniel Knell `_ (contact@danielknell.co.uk) * `David Bonner `_ (dbonner@gmail.com) * `David Chudzicki `_ (dchudz@gmail.com) * `David Mascharka `_ * `Dawn E. Collett `_ * `Derek Gustafson `_ * `Dion Misic `_ (dion.misic@gmail.com) * `Dmitry Dygalo `_ * `Ed Rogers `- * `Eduardo Enriquez `_ (eduardo.a.enriquez@gmail.com) * `El Awbery `_ * `Emmanuel Leblond `_ * `Evan Tey `_ * `Felix Divo `_ * `Felix Grünewald `_ * `Felix Sheldon `_ * `Florian Bruhin `_ * `follower `_ * `Francesc Elies `_ * `Gabe Joseph `_ * `Gary Donovan `_ * `Genevieve Mendoza `_ * `George Macon `_ * `Glenn Lehman `_ * `Graham Williamson `_ * `Grant David Bachman `_ (grantbachman@gmail.com) * `Gregory Petrosyan `_ * `Grzegorz Zieba `_ (g.zieba@erax.pl) * `Grigorios Giannakopoulos `_ * `Hal Blackburn `_ * `Hugo van Kemenade `_ * `Humberto Rocha `_ * `Ilya Lebedev `_ (melevir@gmail.com) * `Israel Fruchter `_ * `Ivan Tham `_ * `Iyassou Shimels `_ * `Jack Massey `_ * `Jakub Nabaglo `_ (j@nab.gl) * `James Lamb `_ * `Jenny Rouleau `_ * `Jens Heinrich `_ * `Jens Tröger `_ * `Jeremy Thurgood `_ * `J.J. Green `_ * `JP Viljoen `_ (froztbyte@froztbyte.net) * `Jochen Müller `_ * `Joseph Weston `_ * `Joey Tuong `_ * `Jonathan Gayvallet `_ (jonathan.gayvallet@orange.com) * `Jonty Wareing `_ (jonty@jonty.co.uk) * `Joshua Boone `_ (joshuaboone4190@gmail.com) * `Joshua Munn `_ (public@elysee-munn.family) * `jmhsi `_ * `Justus Magin `_ * `jwg4 `_ * `Kai Chen `_ (kaichen120@gmail.com) * `Karthikeyan Singaravelan `_ (tir.karthi@gmail.com) * `Katelyn Gigante `_ * `Katrina Durance `_ * `kbara `_ * `Keeri Tramm `_ * `Kristian Glass `_ * `Krzysztof Przybyła `_ * `Kyle Reeve `_ (krzw92@gmail.com) * `Lampros Mountrakis `_ * `Lea Provenzano `_ * `Lee Begg `_ * `Liam DeVoe `_ * `Libor Martínek `_ * `Lisa Goeller `_ * `Louis Taylor `_ * `Luke Barone-Adesi `_ * `Lundy Bernard `_ * `Marco Ricci `_ * `Marco Sirabella `_ * `marekventur `_ * `Marius Gedminas `_ (marius@gedmin.as) * `Markus Unterwaditzer `_ (markus@unterwaditzer.net) * `Mateusz Sokół `_ * `Mathieu Paturel `_ (mathieu.paturel@gmail.com) * `Matt Bachmann `_ (bachmann.matt@gmail.com) * `Matthew Barber `_ (quitesimplymatt@gmail.com) * `Max Nordlund `_ (max.nordlund@gmail.com) * `Maxim Kulkin `_ (maxim.kulkin@gmail.com) * `Mel Seto `_ * `Michel Alexandre Salim `_ (michel@michel-slm.name) * `mulkieran `_ * `Munir Abdinur `_ * `Nathan Goldbaum `_ * `Nicholas Chammas `_ * `Nick Anyos `_ * `Nick Collins ` _ * `Nick Muoh `_ (nickspirit3@gmail.com) * `Nicolas Erni `_ * `Nikita Sobolev `_ (mail@sobolevn.me) * `Oleg Höfling `_ (oleg.hoefling@gmail.com) * `Paul Ganssle `_ (paul@ganssle.io) * `Paul Kehrer `_ * `Paul Lorett Amazona `_ * `Paul Stiverson `_ * `Pax (R. Margret) W. `_ * `Peadar Coyle `_ (peadarcoyle@gmail.com) * `Petr Viktorin `_ * `Phillip Schanely `_ (pschanely@gmail.com) * `Pierre-Jean Campigotto `_ * `Przemek Konopko `_ * `Reagan Lee `_ * `Richard Boulton `_ (richard@tartarus.org) * `Richard Scholtens `_ (richardscholtens2@gmail.com) * `Robert Howlett `_ * `Robert Knight `_ (robertknight@gmail.com) * `Rodrigo Girão Serrão `_ (rodrigo@mathspp.com) * `Rónán Carrigan `_ (rcarriga@tcd.ie) * `Ruben Opdebeeck `_ * `Ryan Soklaski `_ (rsoklaski@gmail.com) * `Ryan Turner `_ (ryan.turner@uber.com) * `Sam Bishop (TechDragon) `_ (sam@techdragon.io) * `Sam Clamons `_ (sclamons@gmail.com) * `Sam Hames `_ * `Sam Watts `_ * `Sangarshanan `_ (sangarshanan1998@gmail.com) * `Sanyam Khurana `_ * `Saul Shanabrook `_ (s.shanabrook@gmail.com) * `Sebastiaan Zeeff `_ (sebastiaan.zeeff@ordina.nl) * `Sharyar Memon `_ (smemon.cal@gmail.com) * `Shaun Read `_ * `Shlok Gandhi `_ (shlok.gandhi@gmail.com) * `Sogata Ray `_ (rayardinanda@gmail.com) * `Stuart Cook `_ * `SuperStormer `_ * `Sushobhit `_ (sushobhitsolanki@gmail.com) * `Tariq Khokhar `_ (tariq@khokhar.net) * `Tessa Bradbury `_ * `Thea Koutsoukis `_ * `Thomas Ball `_ (bomtall1@hotmail.com) * `Thomas Grainge `_ * `Thomas Kluyver `_ (thomas@kluyver.me.uk) * `Tim Martin `_ (tim@asymptotic.co.uk) * `Tom McDermott `_ (sponster@gmail.com) * `Tom Milligan `_ (code@tommilligan.net) * `Tyler Gibbons `_ (tyler.gibbons@flexport.com) * `Tyler Nickerson `_ * `Vidya Rani `_ (vidyarani.d.g@gmail.com) * `Vince Reuter `_ (vince.reuter@gmail.com) * `Vincent Michel `_ (vxgmichel@gmail.com) * `Viorel Pluta `_ (viopluta@gmail.com) * `Vytautas Strimaitis `_ * `Will Hall `_ (wrsh07@gmail.com) * `Will Thompson `_ (will@willthompson.co.uk) * `Wilfred Hughes `_ * `Yiyang Zhan `_ * `Zac Hatfield-Dodds `_ (zac.hatfield.dodds@gmail.com) * `Zebulun Arendsee `_ (zbwrnz@gmail.com) ================================================ FILE: CITATION.cff ================================================ cff-version: 1.2.0 message: | If you use Hypothesis as part of a published research project, please cite our paper in the Journal of Open Source Software: Text: MacIver et al., (2019). Hypothesis: A new approach to property-based testing. Journal of Open Source Software, 4(43), 1891, https://doi.org/10.21105/joss.01891 BibTeX: @article{MacIver2019Hypothesis, journal = {Journal of Open Source Software}, doi = {10.21105/joss.01891}, issn = {2475-9066}, number = {43}, publisher = {The Open Journal}, title = {Hypothesis: A new approach to property-based testing}, url = {http://dx.doi.org/10.21105/joss.01891}, volume = {4}, author = {MacIver, David and Hatfield-Dodds, Zac and Contributors, Many}, pages = {1891}, date = {2019-11-21}, year = {2019}, month = {11}, day = {21}, } To reference a particular version of Hypothesis as a software artifact, you can use the version-specific DOIs we create for each release under https://doi.org/10.5281/zenodo.1412597 preferred-citation: title: 'Hypothesis: A new approach to property-based testing' date-released: 2019-11-21 type: article doi: 10.21105/joss.01891 authors: - family-names: MacIver given-names: David R. orcid: https://orcid.org/0000-0002-8635-3223 affiliation: Imperial College London - family-names: Hatfield-Dodds given-names: Zac orcid: https://orcid.org/0000-0002-8646-8362 affiliation: Australian National University - name: "many other contributors" # Citation metadata for the software itself, as required by the CFF spec doi: 10.5281/zenodo.1412597 # Version-independent DOI for the software archive title: 'Hypothesis: Property-Based Testing for Python' repository-code: https://github.com/HypothesisWorks/hypothesis license: MPL-2.0 authors: - family-names: MacIver given-names: David R. orcid: https://orcid.org/0000-0002-8635-3223 affiliation: Imperial College London - family-names: Hatfield-Dodds given-names: Zac orcid: https://orcid.org/0000-0002-8646-8362 affiliation: Australian National University - name: "many other contributors" ================================================ FILE: CONTRIBUTING.rst ================================================ ============= Contributing ============= First off: It's great that you want to contribute to Hypothesis! Thanks! --------------------------------------- Just tell me how to make a pull request --------------------------------------- 1. Make your change and ensure it has adequate tests 2. Create ``hypothesis-python/RELEASE.rst`` with ``RELEASE_TYPE: patch`` for small bugfixes, or ``minor`` for new features. See ``RELEASE-sample.rst`` as an example. 3. Add yourself to the list in ``AUTHORS.rst`` and open a PR! For more detail, read on; for even more, continue to the ``guides/`` directory! ------------------ Ways to Contribute ------------------ Hypothesis is a mature yet active project. This means that there are many ways in which you can contribute. For example, it's super useful and highly appreciated if you do any of: * Submit bug reports * Submit feature requests * Write about Hypothesis * Give a talk about Hypothesis * Build libraries and tools on top of Hypothesis outside the main repo * Submit PRs If you build a Hypothesis strategy that you would like to be more widely known please add it to the list of external strategies by preparing a PR against the docs/strategies.rst file. If you find an error in the documentation, please feel free to submit a PR that fixes the error. Spot a tyop? Fix it up and send us a PR! You can read more about how we document Hypothesis in ``guides/documentation.rst`` The process for submitting source code PRs is generally more involved (don't worry, we'll help you through it), so do read the rest of this document. If you're planning a larger change, the contributor guides (in the ``guides/`` directory) will make sure you're on the right track. ---------------------------------- Installing from source and testing ---------------------------------- If you want to install directly from the source code (e.g. because you want to make changes and install the changed version) you can do this with: .. code:: bash pip install -r requirements/test.in pip install -r requirements/tools.in pip install -e hypothesis-python/ # You don't need to run the tests, but here's the command: pytest hypothesis-python/tests/cover/ You may wish to do all of this in a `virtualenv `_. For example: .. code:: bash python3 -m venv .venv source .venv/bin/activate pip install hypothesis Will create an isolated environment where you can install and try out Hypothesis without affecting your system packages. ----------------------- Copyright and Licensing ----------------------- It's important to make sure that you own the rights to the work you are submitting. If it is done on work time, or you have a particularly onerous contract, make sure you've checked with your employer. All work in Hypothesis is licensed under the terms of the `Mozilla Public License, version 2.0 `_. By submitting a contribution you are agreeing to licence your work under those terms. Finally, if it is not there already, add your name (and a link to your GitHub and email address if you want) to the list of contributors found in AUTHORS.rst, in alphabetical order. It doesn't have to be your "real" name (whatever that means), any sort of public identifier is fine. In particular a GitHub account is sufficient. ----------------------- The actual contribution ----------------------- OK, so you want to make a contribution and have sorted out the legalese. What now? First off: If you're planning on implementing a new feature, talk to us first! Come `join us on the mailing list `_, or open an issue. If it's really small feel free to open a work in progress pull request sketching out the idea, but it's best to get feedback from the Hypothesis maintainers before sinking a bunch of work into it. If you're working on an existing issue, leave a comment so we can try to avoid duplicating your work before you open a pull request. In general work-in-progress pull requests are totally welcome if you want early feedback or help with some of the tricky details. Don't be afraid to ask for help. In order to get merged, a pull request will have to have a green build (naturally) and to be approved by a Hypothesis maintainer (and, depending on what it is, possibly specifically by DRMacIver). Most pull requests will also need to `write a changelog entry in hypothesis-python/RELEASE.rst `_. The review process is the same one that all changes to Hypothesis go through, regardless of whether you're an established maintainer or entirely new to the project. It's very much intended to be a collaborative one: It's not us telling you what we think is wrong with your code, it's us working with you to produce something better together. We have `a lengthy check list `_ of things we look for in a review. Feel free to have a read of it in advance and go through it yourself if you'd like to. It's not required, but it might speed up the process. Once your pull request has a green build and has passed review, it will be merged to master fairly promptly. This will immediately trigger a release! Don't be scared. If that breaks things, that's our fault not yours - the whole point of this process is to ensure that problems get caught before we merge rather than after. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pull request or external package? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ New strategies can be added to Hypothesis, or published as an external package on PyPI - either is fine for most strategies. If in doubt, ask! It's generally much easier to get things working outside, because there's more freedom to experiment and fewer requirements in stability and API style. We're happy to review and help with external packages as well as pull requests; several parts of Hypothesis started life outside and were integrated later (with permission, of course). To help people find your package, please use the `Framework :: Hypothesis `__ `trove classifier `__. We also recommend naming your package in the pattern of ``hypothesis-graphql`` and ``hypothesis-protobuf`` on PyPI. On the other hand, being inside gets you access to some deeper implementation features (if you need them) and better long-term guarantees about maintenance. We particularly encourage pull requests for new composable primitives that make implementing other strategies easier, or for widely used types in the Python standard library. Strategies for other things are also welcome; anything with external dependencies just goes in ``hypothesis.extra``. ~~~~~~~~~ The build ~~~~~~~~~ The build is driven by a ``build.sh`` shell script, which delegates to a custom Python-based build system. Actually running the tests is managed by `tox `_, but the build system will call out to the relevant tox environments so you mostly don't have to know anything about that unless you want to make changes to the test config. You also mostly don't need to know anything about the build system except to type ``./build.sh`` followed by the name of the task you want to run. All of it will be checked on CI so you don't *have* to run anything locally, but you might find it useful to do so: A full CI run can take up to twenty minutes, so running a smaller set of tests locally can be helpful. The build system should be "fairly" portable, but is currently only known to work on Linux or macOS. It *might* work on a BSD or on Windows with cygwin installed, but it hasn't been tried. Windows with WSL does work, as for Linux, and since OS-specific issues are rare for Hypothesis that's pretty useful. If you try it and find it doesn't work, please do submit patches to fix that. Some notable commands: ``./build.sh check-coverage`` will verify 100% code coverage by running a curated subset of the test suite. ``./build.sh check-py311`` (etc.) will run most of the test suite against a particular python version. ``./build.sh format`` will reformat your code according to the Hypothesis coding style. You should use this before each commit ideally, but you only really have to use it when you want your code to be ready to merge. You can also use ``./build.sh check-format``, which will run format and some linting and will then error if you have a git diff. Note: This will error even if you started with a git diff, so if you've got any uncommitted changes this will necessarily report an error. Run ``./build.sh tasks`` for a list of all supported build task names. Note: The build requires a lot of different versions of python, so rather than have you install them yourself, the build system will install them itself in a local directory. This means that the first time you run a task you may have to wait a while as the build downloads and installs the right version of python for you. ~~~~~~~~~~~~~ Running Tests ~~~~~~~~~~~~~ The tasks described above will run all of the tests (e.g. ``check-py311``). But the ``tox`` task will give finer-grained control over the test runner. At a high level, the task takes the form: .. code-block:: ./build.sh tox py311-custom 3.11.3 [tox args] -- [pytest args] Namely, first provide the tox environment (see ``tox.ini``), then the python version to test with, then any ``tox`` or ``pytest`` args as needed. For example, to run all of the tests in the file ``tests/nocover/test_conjecture_engine.py`` with python 3.12: .. code-block:: ./build.sh tox py312-custom 3.12.7 -- tests/nocover/test_conjecture_engine.py See the ``tox`` docs and ``pytest`` docs for more information: * https://docs.pytest.org/en/latest/how-to/usage.html * https://tox.wiki/en/latest/config.html#cli ^^^^^^^^^^^ Test Layout ^^^^^^^^^^^ See ``hypothesis-python/tests/README.rst`` ^^^^^^^^^^^^^^^^ Useful Arguments ^^^^^^^^^^^^^^^^ Some useful arguments to pytest include: * You can pass ``-n 0`` to turn off ``pytest-xdist``'s parallel test execution. Sometimes for running just a small number of tests its startup time is longer than the time it saves (this will vary from system to system), so this can be helpful if you find yourself waiting on test runners to start a lot. * You can use ``-k`` to select a subset of tests to run. This matches on substrings of the test names. For example ``-kfoo`` will only run tests that have "foo" as a substring of their name. You can also use composite expressions here. e.g. ``-k'foo and not bar'`` will run anything containing foo that doesn't also contain bar. `More information on how to select tests to run can be found in the pytest documentation `__. ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2013, David R. MacIver All code in this repository except where explicitly noted otherwise is released under the Mozilla Public License v 2.0. You can obtain a copy at https://mozilla.org/MPL/2.0/. Some code in this repository comes from other projects. Where applicable, the original copyright and license are noted and any modifications made are released dual licensed with the original license. ================================================ FILE: README.md ================================================
# Hypothesis * [Website](https://hypothesis.works/) * [Documentation](https://hypothesis.readthedocs.io/en/latest/) * [Source code](https://github.com/hypothesisWorks/hypothesis/) * [Contributing](https://github.com/HypothesisWorks/hypothesis/blob/master/CONTRIBUTING.rst) * [Community](https://hypothesis.readthedocs.io/en/latest/community.html) Hypothesis is the property-based testing library for Python. With Hypothesis, you write tests which should pass for all inputs in whatever range you describe, and let Hypothesis randomly choose which of those inputs to check - including edge cases you might not have thought about. For example: ```python from hypothesis import given, strategies as st @given(st.lists(st.integers())) def test_matches_builtin(ls): assert sorted(ls) == my_sort(ls) ``` This randomized testing can catch bugs and edge cases that you didn't think of and wouldn't have found. In addition, when Hypothesis does find a bug, it doesn't just report any failing example — it reports the simplest possible one. This makes property-based tests a powerful tool for debugging, as well as testing. For instance, ```python def my_sort(ls): return sorted(set(ls)) ``` fails with the simplest possible failing example: ``` Falsifying example: test_matches_builtin(ls=[0, 0]) ``` ### Installation To install Hypothesis: ``` pip install hypothesis ``` There are also [optional extras available](https://hypothesis.readthedocs.io/en/latest/extras.html). ================================================ FILE: brand/README.md ================================================ # Logos and other pretty things Hypothesis has a beautiful logo, thanks to the generous work of Libby Berrie in [issue #1519](https://github.com/HypothesisWorks/hypothesis/issues/1519). The [CogWorks class of 2019](https://github.com/CogWorksBWSI) named the dragonfly "Scout", as a job description and after *To Kill a Mockingbird*. General guidelines: - Prefer vector (`.svg`) formats to raster formats (`.png`) wherever possible. However, some viewers don't handle the layers well - e.g. the left eye might be drawn in front of the head - in which case you might prefer `.png`. - We consider the rainbow version to be canonical. The blue variant is provided for cases such as monochrome versions or printing with a limited palette. With that in mind, you are welcome to use these logos to refer to Hypothesis - and if you're not sure whether a specific use is OK, please get in touch and ask! For example, we often bring Hypothesis stickers to conferences but can't make it to everything. If you want to print your own Hypothesis stickers, upload `sticker.png` to [StickerMule](https://www.stickermule.com/custom-stickers) and pick one of the die-cut vinyl options - that's how we get ours! ![Hypothesis stickers suitable for your laptop](./stickers.jpg) ## Colour palette in GIMP format A [colour palette in GIMP format](hypothesis.gpl) (`.gpl`) is also provided with the intent of making it easier to produce graphics and documents which reuse the colours in the Hypothesis Dragonfly logo by Libby Berrie. The `hypothesis.gpl` file should be copied or imported to the appropriate location on your filesystem. For example: - `/usr/share/inkscape/palettes/` for Inkscape on Ubuntu 18.08 - Edit -> Colors -> Import... then select the `hypothesis.gpl` file in Scribus on Ubuntu 18.08 - Windows -> Dockable Dialogs -> Palettes -> Palettes Menu -> Add Palette -> Import from file... then select the `hypothesis.gpl` file in GIMP on Ubuntu 18.08 Once imported, the colour palette is then available for easy manipulation of colours within the user interface. Inkscape: ![Inkscape showing Hypothesis colour palette](inkscape.png) GIMP: ![GIMP showing Hypothesis colour palette](gimp.png) ================================================ FILE: brand/hypothesis.gpl ================================================ GIMP Palette Name: Hypothesis Palette Columns: 1 # generated by @kathyreid # from the Dragonfly logo created by @libbyberrie # the purpose of this file is to make it easy to import colours into # Inkscape, GIMP, Scribus etc # Colours deliberately prefixed with `Hypothesis.xxx` to appear grouped in apps 0 0 0 Black (#000000) 255 255 255 White (#FFFFFF) 165 100 250 Hypothesis.Violet (#A564FA) 98 183 255 Hypothesis.DuckEggBlue (#62B7FF) 136 255 127 Hypothesis.AppleGreen (#88FF7F) 255 252 54 Hypothesis.WarmYellow (#FFFC36) 255 202 120 Hypothesis.Apricot (#FFCA78) 255 128 128 Hypothesis.LightCoral (#FF8080) 163 255 247 Hypothesis.Turquoise (#A3FFF7) 163 233 255 Hypothesis.SkyBlue (#A3E9FF) 128 211 231 Hypothesis.DarkSky (#80D3E7) 101 195 213 Hypothesis.MediumTeal (#65C3D5) 92 201 236 Hypothesis.OceanBlue (#5CC9EC) 39 135 178 Hypothesis.DarkTeal (#2787B2) 87 83 139 Hypothesis.OceanPurple (#57538B) 68 66 95 Hypothesis.DarkOceanPurple (#44425F) ================================================ FILE: build.sh ================================================ #!/usr/bin/env bash # This script is here to bootstrap the Hypothesis build process into a working # version of Python, then hand over to the actual Hypothesis build runner (which # is written in Python instead of bash). if [ -n "${CI:-}" ] ; then echo "::group::Build setup" ; fi set -o xtrace set -o errexit set -o nounset ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" export HYPOTHESIS_ROOT="$ROOT" SCRIPTS="$ROOT/tooling/scripts" # shellcheck source=tooling/scripts/common.sh source "$SCRIPTS/common.sh" PYTHON_VERSION="3.14.3" if [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] || [ -n "${CLAUDECODE-}" ] ; then # We're on GitHub Actions, Codespaces, or Claude Code and already have a suitable Python PYTHON=$(command -v python3 || command -v python) else # Otherwise, we install it from scratch # NOTE: tooling keeps this version in sync with ci_version in tooling "$SCRIPTS/ensure-python.sh" "$PYTHON_VERSION" PYTHON=$(pythonloc "$PYTHON_VERSION")/bin/python fi TOOL_REQUIREMENTS="$ROOT/requirements/tools.txt" # append PYTHON_VERSION to bust caches when we upgrade versions TOOL_HASH=$( (cat "$TOOL_REQUIREMENTS" && echo "$PYTHON_VERSION") | "$PYTHON" "$SCRIPTS/tool-hash.py") TOOL_VIRTUALENV="$VIRTUALENVS/build-$TOOL_HASH" TOOL_PYTHON="$TOOL_VIRTUALENV/bin/python" export PYTHONPATH="$ROOT/tooling/src" if ! "$TOOL_PYTHON" -m hypothesistooling check-installed ; then rm -rf "$TOOL_VIRTUALENV" if [ -n "${CLAUDECODE-}" ] ; then # Claude Code: use venv (available) and skip pip upgrades (debian-managed) "$PYTHON" -m venv "$TOOL_VIRTUALENV" else "$PYTHON" -m pip install --upgrade pip "$PYTHON" -m pip install --upgrade virtualenv "$PYTHON" -m virtualenv "$TOOL_VIRTUALENV" fi "$TOOL_PYTHON" -m pip install --no-warn-script-location -r requirements/tools.txt fi if [ -n "${CI:-}" ] ; then echo "::endgroup::" ; fi "$TOOL_PYTHON" -m hypothesistooling "$@" ================================================ FILE: guides/README.md ================================================ # Guides for Hypothesis Development This is a general collection of useful documentation for people working on Hypothesis. It is separate from the main documentation because it is not much use if you are merely *using* Hypothesis. It's purely for working on it, and aimed more at maintainers than casual contributors. ================================================ FILE: guides/api-style.rst ================================================ =============== House API Style =============== Here are some guidelines for how to write APIs so that they "feel" like a Hypothesis API. This is particularly focused on writing new strategies, as that's the major place where we add APIs, but also applies more generally. Note that it is not a guide to *code* style, only API design. The Hypothesis style evolves over time, and earlier strategies in particular may not be consistent with this style, and we've tried some experiments that didn't work out, so this style guide is more normative than descriptive and existing APIs may not match it. Where relevant, backwards compatibility is much more important than conformance to the style. We also encourage `third-party extensions `_ to follow this style guide, for consistent and user-friendly testing APIs, or get in touch to discuss changing it if it doesn't fit their domain. ~~~~~~~~~~~~~~~~~~ General Guidelines ~~~~~~~~~~~~~~~~~~ * When writing extras modules, consistency with Hypothesis trumps consistency with the library you're integrating with. * *Absolutely no subclassing as part of the public API* * We should not strive too hard to be pythonic, but if an API seems weird to a normal Python user we should see if we can come up with an API we like as much but is less weird. * Code which adds a dependency on a third party package should be put in a hypothesis.extra module. * Complexity should not be pushed onto the user. An easy to use API is more important than a simple implementation. ~~~~~~~~~~~~~~~~~~~~~~~~~ Guidelines for strategies ~~~~~~~~~~~~~~~~~~~~~~~~~ * A strategy function should be somewhere between a recipe for how to build a value and a range of valid values. * It should not include distribution hints. The arguments should only specify how to produce a valid value, not statistical properties of values. * Strategies should try to paper over non-uniformity in the underlying types as much as possible (e.g. ``hypothesis.extra.numpy`` has a number of workarounds for numpy's odd behaviour around object arrays). * Strategies should usually default to allowing generation of any example they can support. The only exceptions should be cases where certain inputs would trigger test failures which are almost never of interest: currently just non-UTF8 characters in ``st.text()``, and Numpy array shapes with zero dimensions or sides of length zero. In each case opting in should be trivial. ~~~~~~~~~~~~~~~~~ Argument handling ~~~~~~~~~~~~~~~~~ We have a reasonably distinctive style when it comes to handling arguments: * Arguments must be validated to the greatest extent possible. Hypothesis should reject bad arguments with an InvalidArgument error, not fail with an internal exception. * We make extensive use of default arguments. If an argument could reasonably have a default, it should. * Exception to the above: strategies for collection types should *not* have a default argument for element strategies. * Arguments which have a default value should also be keyword-only, with the exception of ``min_value`` and ``max_value`` (see "Argument Names" below). * ``min_value`` and ``max_value`` should default to None for unbounded types such as integers, and the minimal or maximal values for bounded types such as datetimes. ``floats()`` is an explicit exception to this rule due to special handling for infinities and not-a-number. * Interacting arguments (e.g. arguments that must be in a particular order, or where at most one is valid, or where one argument restricts the valid range of the other) are fine, but when this happens the behaviour of defaults should automatically be adjusted. e.g. if the normal default of an argument would become invalid, the function should still do the right thing if that default is used. * Where the actual default used depends on other arguments, the default parameter should be None. * It's worth thinking about the order of arguments: the first one or two arguments are likely to be passed positionally, so try to put values there where this is useful and not too confusing. * When adding arguments to strategies, think carefully about whether the user is likely to want that value to vary often. If so, make it a strategy instead of a value. In particular if it's likely to be common that they would want to write ``some_strategy.flatmap(lambda x: my_new_strategy(argument=x))`` then it should be a strategy. * Arguments should not be "a value or a strategy for generating that value". If you find yourself inclined to write something like that, instead make it take a strategy. If a user wants to pass a value they can wrap it in a call to ``just``. * If a combination of arguments make it impossible to generate anything, ``raise InvalidArgument`` instead of ``return nothing()``. Returning the null strategy is conceptually nice, but can lead to silently dropping parts from composed strategies and thus unexpectedly weak tests. ~~~~~~~~~~~~~~ Function Names ~~~~~~~~~~~~~~ We don't have any real consistency here. The rough approach we follow is: * Names are `snake_case` as is standard in Python. * Strategies for a particular type are typically named as a plural name for that type. Where that type has some truncated form (e.g. int, str) we use a longer form name. * Other strategies have no particular common naming convention. ~~~~~~~~~~~~~~ Argument Names ~~~~~~~~~~~~~~ We should try to use the same argument names and orders across different strategies wherever possible. In particular: * For collection types, the element strategy (or strategies) should always be the first arguments. Where there is only one element strategy it should be called ``elements`` (but e.g. ``dictionaries`` has element strategies named ``keys`` and ``values`` and that's fine). * For ordered types, the first two arguments should be a lower and an upper bound. They should be called ``min_value`` and ``max_value``. * Collection types should have a ``min_size`` and a ``max_size`` parameter that controls the range of their size. ``min_size`` should default to zero and ``max_size`` to ``None`` (even if internally it is bounded). ~~~~~~~~~~~~~~~ Deferred Errors ~~~~~~~~~~~~~~~ As far as is reasonable, functions should raise errors when the test is run (typically by deferring them until you try to draw from the strategy), not when they are called. This mostly applies to strategy functions and some error conditions in ``@given`` itself. Generally speaking this should be taken care of automatically by use of the ``@defines_strategy`` decorator. We do not currently do this for the ``TypeError`` that you will get from calling the function incorrectly (e.g. with invalid keyword arguments or missing required arguments). In principle we could, but it would result in much harder to read function signatures, so we would be trading off one form of comprehensibility for another, and so far that hasn't seemed to be worth it. The main reasons for preferring this style are: * Errors at test import time tend to throw people and be correspondingly hard for them to debug. There's an expectation that errors in your test code result in failures in your tests, and the fact that that test code happens to be defined in a decorator doesn't seem to change that expectation for people. * Things like deprecation warnings etc. localize better when they happen inside the test - test runners will often swallow them or put them in silly places if they're at import time, but will attach any output that happens in the test to the test itself. * There are a lot of cases where raising an error, deprecation warning, etc. is *only* possible in a test - e.g. if you're using the inline style with `data `_, or if you're using `flatmap `_ or `@composite `_ then the strategy won't actually get evaluated until we run the test, so that's the only place they can happen. It's nice to be consistent, and it's weird if sometimes strategy errors result in definition time errors and sometimes they result in test errors. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Inferring strategies from specifications ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Functions which infer a strategy from some specification or schema are both convenient for users, and offer a single source of truth about what inputs are allegedly valid and actually tested for correctness. * Such functions should be named "``from_foo()``" and the first argument should be the thing from which a strategy is inferred - like ``st.from_type()``, ``st.from_regex()``, ``extra.lark.from_lark()``, ``extra.numpy.from_dtype()``, etc. Any other arguments should be optional keyword-only parameters. * There should be a smooth path to customise *parts* of an inferred strategy, i.e. not require the user to start from scratch if they need something a little more specific. ``from_dtype()`` does this well; ``from_type()`` supports it by `pointing users to builds() instead `_. * Where practical, ensure that the ``repr`` of the returned strategy shows how it was constructed - only using e.g. ``@st.composite`` if required. For example, ``repr(from_type(int)) == "integers()"``. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A catalogue of current violations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following are places where we currently deviate from this style. Some of these should be considered targets for deprecation and/or improvement. * ``hypothesis.extra.numpy`` has some arguments which can be either strategies or values. * ``hypothesis.extra.numpy`` assumes arrays are fixed size and doesn't have ``min_size`` and ``max_size`` arguments (but this is probably OK because of more complicated shapes of array). * ``hypothesis.stateful`` is a great big subclassing based train wreck. ================================================ FILE: guides/documentation.rst ================================================ ===================================== The Hypothesis Documentation Handbook ===================================== Good documentation can make the difference between good code and useful code - and Hypothesis is written to be used, as widely as possible. This is a working document-in-progress with some tips for how we try to write our docs, with a little of the what and a bigger chunk of the how. If you have ideas about how to improve these suggestions, meta issues or pull requests are just as welcome as for docs or code :D ---------------------------- What docs should be written? ---------------------------- All public APIs should be comprehensively described. If the docs are confusing to new users, incorrect or out of date, or simply incomplete - we consider all of those to be bugs; if you see them please raise an issue and perhaps submit a pull request. That's not much advice, but it's what we have so far. ------------ Using Sphinx ------------ We use `the Sphinx documentation system `_ to convert the .rst files into html with formatting and cross-references. Without repeating the docs for Sphinx, here are some tips: - When documenting a Python object (function, class, module, etc.), you can use autodoc to insert and interpret the docstring. - When referencing a function, you can insert a reference to a function as (eg) ``:func:`hypothesis.given`\``, which will appear as ``hypothesis.given()`` with a hyperlink to the appropriate docs. You can show only the last part (unqualified name) by adding a tilde at the start, like ``:func:`~hypothesis.given`\ `` -> ``given()``. Finally, you can give it alternative link text in the usual way: ``:func:`other text `\ `` -> ``other text``. - For the formatting and also hyperlinks, all cross-references should use the Sphinx cross-referencing syntax rather than plain text. ----------------- Changelog Entries ----------------- `Hypothesis does continuous deployment `_, where every pull request that touches ``./src`` results in a new release. That means every contributor gets to write their changelog! A changelog entry should be written in a new ``RELEASE.rst`` file in the `hypothesis-python` directory. The first line of the file specifies the component of the version number that will be updated, according to our `semantic versioning `_ policy. - ``RELEASE_TYPE: major`` is for breaking changes, and will only be used by the core team after extensive discussion. - ``RELEASE_TYPE: minor`` is for anything that adds to the public (ie documented) API, changes an argument signature, or adds a new deprecation or health check. Minor (or patch) releases **must not** cause errors in any code that runs without errors on an earlier version of Hypothesis, using only the public API. Silent errors *may* be converted to noisy errors, but generally we prefer to issue a deprecation warning and use the new behaviour if possible. This stability policy only applies to use of Hypothesis itself, not the results of user-written tests that use Hypothesis. - ``RELEASE_TYPE: patch`` is for changes that are not visible in the public interface, from improving a docstring to backwards-compatible improvements in shrinking behaviour. This first line will be removed from the final change log entry. The remaining lines are the actual changelog text for this release, which should: - concisely describe what changed and why - use Sphinx cross-references to any functions or classes mentioned - if closing an issue, mention it with the ``:issue:`` role to generate a link - finish with a note of thanks from the maintainers: "Thanks to for this bug fix / feature / contribution" (depending on which it is). If this is your first contribution, don't forget to add yourself to AUTHORS.rst! ================================================ FILE: guides/internals.rst ================================================ =================================== How to Work on Hypothesis Internals =================================== This is a guide to how to work on Hypothesis internals, with a particular focus on helping people who are new to it. Right now it is very rudimentary and is intended primarily for people who are looking to get started writing shrink passes as part of our `current outreach program to get more people doing that `_, but it will expand over time. ------------------------ Bird's Eye View Concepts ------------------------ The core engine of Hypothesis is called Conjecture. The "fundamental idea" of Conjecture is that you can represent an arbitrary randomized test case as the sequence of bytes read from the pseudo-random number generator (PRNG) that produced it. Whenever the test did something "random" it actually read the next bytes and did what they told it to do. But those bytes didn't *have* to come from a PRNG, and we can run the test given any byte sequence we like. By manipulating the choice of bytes, we can achieve more interesting effects than pure randomness would allow us to do, while retaining the power and ease of use of random testing. The greatest strength of this idea is that we have a single source of truth for what an example should look like: Every byte sequence is one that *could* have come from a PRNG, and thus is a valid thing to try for our test. The only ways it can fail to be a valid test input are for it to be too short or for it to not satisfy one of the test's preconditions, and both are easily detectable. The idea of shrinking in particular is that once we have this representation, we can shrink arbitrary test cases based on it. We try to produce a string that is *shortlex minimal*. What this means is that it has the shortest possible length and among those strings of minimal length is lexicographically (i.e. the normal order on strings - find the first byte at which they differ and use that to decide) smallest. Ideally we could think of the shrinker as a generic function that takes a string satisfying some predicate and returns the shortlex minimal string that also satisfies it. We depart from this ideal in two ways: * we can only *approximate* such a minimal string. Finding the actual minimum is intractable in general. * we are only interested in minimizing things where the predicate goes through the Hypothesis API, which lets us track how the data is used and use that to guide the process. We then use a number of different transformations of the string to try and reduce our input. These vary from principled general transformations to shameless hacks that special case something we need to work well. One such example of a hack is the handling of floating point numbers. There are a couple of lexicographic shrinks that are always valid but only really make sense for our particular encoding of floats. We check if we're working on something that is of the right size to be a float and apply those transformations regardless of whether it is actually meant to be a float. Worst case scenario it's not a float and they don't work, and we've run a few extra test cases. -------------------------- Useful Files to Know About -------------------------- The code associated with Conjecture lives in `src/hypothesis/internal/conjecture `_. There are a number of files in there, but the most important ones are ``engine.py`` and ``data.py``. ``data.py`` defines the core type that is used to represent test cases, and ``engine.py`` contains the main driver for deciding what test cases to run. There is also ``minimizer.py``, which contains a general purpose lexicographic minimizer. This is responsible for taking some byte string and a predicate over byte strings and producing a string of the same length which is lexicographically smaller. Unlike the shrinker in general, this *is* supposed to work on arbitrary predicates and doesn't know anything about the testing API. We typically apply this to subsets of the bytes for a test input with a predicate that knows how to integrate those subsets into a larger test. This is the part of the code that means we can do things like replacing an integer with a smaller one. ------- Testing ------- For general information about how to test Hypothesis, take a look at the `testing guide `_, but there are a couple of areas that it's worth specifically highlighting for making changes to the engine: The first is `tests/conjecture/ `_, which is a set of unit tests designed to put the engine into particular scenarios to exercise specific behaviours, with a goal of achieving 100% coverage on it in isolation (though it currently does not quite achieve that for some specific edge cases. We may fix and enforce this later). The other set of tests that are worth knowing about are the quality tests, in `tests/quality `_. These assert specific hard to satisfy properties about the examples that Hypothesis finds - either their existence, or something about the final shrunk result. ----------------------- Engine Design Specifics ----------------------- There are a couple of code patterns that are mostly peculiar to Conjecture that you may not have encountered before and are worth being aware of. ~~~~~~~~~~~~~~~~~~~~ Search State Objects ~~~~~~~~~~~~~~~~~~~~ There are a number of cases where we find ourself with a user-provided function (where the "user" might still be something that is entirely our code) and we want to pass a whole bunch of different examples to it in order to achieve some result. Currently this includes each of the main engine, the Shrinker (in ``engine.py``) and the minimizer, but there are likely to be more in future. We typically organise such things in terms of an object that you create with the function and possibly an initial argument that stores these on self and has some ``run`` or similar method. They then run for a while, repeatedly calling the function they were given. Generally speaking they do not call the function directly, but instead wrap calls to it. This allows them to implement a certain amount of decision caching, e.g. avoiding trying the same shrink twice, but also gives us a place where we can update metadata about the search process. For objects whose goal is some form of optimisation (Shrinker, Minimizer) one of the pieces of metadata they will typically track is a "current target". This is typically the best example they have seen so far. By wrapping every call to the predicate, we ensure that we never miss an example even when we're passing through other things. For objects whose goal is some broader form of search (currently only ``ConjectureRunner``) this also allows them to keep track of *other* examples of interest. For example, as part of our multiple bug discovery, ``ConjectureRunner`` keeps track of the smallest example of each distinct failure that it has seen, and updates this automatically each time the test function is called. This means that if during shrinking we "slip" and find a different bug than the one we started with, we will *not* shrink to that, but it will get remembered by the runner if it was either novel or better than our current example. ~~~~~~~~~~~ Weird Loops ~~~~~~~~~~~ The loops inside a lot of the engine look very strange and unidiomatic. For example: .. code-block:: python i = 0 while i < len(self.intervals): u, v = self.intervals[i] if not self.incorporate_new_buffer( self.shrink_target.buffer[:u] + self.shrink_target.buffer[v:] ): i += 1 The more natural way to write this in Python would be: .. code-block:: python for u, v in self.intervals: self.incorporate_new_buffer( self.shrink_target.buffer[:u] + self.shrink_target.buffer[v:] ) This is not equivalent in this case, and would exhibit the wrong behaviour. Every time ``incorporate_new_buffer`` succeeds, it changes the shape of the current shrink target. This consequently changes the shape of intervals, both its particular values and its current length - on each loop iteration the loop might stop either because ``i`` increases or because ``len(self.intervals)`` decreases. We do not reset ``i`` to zero on success, as this would cause us to retry deleting things that we have already tried. This *might* work, but is less likely to. In the event that none of the earlier deletions succeed, this causes us to do retry the entire prefix uselessly, which can result in a pass taking O(n^2) time to do O(n) deletions. An additional quirk is that we only increment ``i`` on failure. The reason for this is that if we successfully deleted the current interval then the interval in position ``i`` has been replaced with something else, which is probably the next thing we would have tried deleting if we hadn't succeeded (or something like it), so we don't want to advance past it. This is specific to deletion: If we are just replacing the contents of something then we expect it to still be in the same place, so there we increment unconditionally. Examples of this include ``zero_draws`` and ``minimize_individual_blocks``. ------------ The Shrinker ------------ The shrinking part of Hypothesis is organised into a single class called ``Shrinker`` that lives in ``hypothesis/internal/conjecture/shrinker.py``. Its job is to take an initial ``ConjectureData`` object and some predicate that it satisfies, and to try to produce a simpler ``ConjectureData`` object that also satisfies that predicate. The search process mostly happens in the ``shrink`` method, which tries various shrink passes in the ``greedy_shrink`` method and then reports on the outcome. For details, you are strongly encouraged to read the source code. It is very well commented, and as the subject of active research often has newer techniques than are documented here. ~~~~~~~~~~~~~ Search Passes ~~~~~~~~~~~~~ Search passes are methods on the ``Shrinker`` class. They are designed to take the current shrink target and try a number of things that might be sensible shrinks of it. Typically the design of a search pass is that it should always try to run to completion rather than exiting as soon as it's found something good, but that it shouldn't retry things that are too like stuff it has already tried just because something worked. So for example in the above loop, we try deleting each interval (these roughly correspond to regions of the input that are responsible for some particular value or small number of adjacent values). When we succeed, we keep going and try deleting more intervals, but we don't try to delete any intervals before the current index. The reason for this is that retrying things from the beginning might work but probably won't. Thus if we restarted every time we made a change we would end up doing a lot of useless work. Additionally, they are *more* likely to work after other shrink passes have run because frequently other changes are likely to unlock changes in the current pass that were previously impossible. e.g. when we reorder some examples we might make a big region deletable that previously contained something critical to the relevant behaviour of the test but is now just noise. Because the shrinker runs in a big loop, if we've made progress the shrink pass will always be run again (assuming we don't hit some limit that terminates the shrink early, but by making the shrinker better we try to ensure that that never happens). This means that we will always get an opportunity to start again later if we made progress, and if we didn't make progress we've tried everything anyway. ~~~~~~~~~~~~~~~~~~~~~~~ Expensive Shrink Passes ~~~~~~~~~~~~~~~~~~~~~~~ We have a bunch of search passes that are considered "expensive". Typically this means "quadratic or worse complexity". When shrinking we initially don't run these, and the first time that we get to the end of our main passes and have failed to make the input any smaller, we then turn them on. This allows the shrinker to switch from a good but slightly timid mode while its input is large into a more aggressive DELETE ALL THE THINGS mode once that stops working. By that point we've usually made our input small enough that quadratic complexity is acceptable. We turn these on once and then they stay on. The reason for this is to avoid a "flip-flopping" scenario where an expensive pass unlocks one trivial change that the cheap passes can find and then they get stuck again and have to do an extra useless run through the passes to prove that. ~~~~~~~~~~~~~~~~~~~~~~ Adaptive Shrink Passes ~~~~~~~~~~~~~~~~~~~~~~ A useful trick that some of the shrink passes use is to try a thing and if it doesn't work take a look at what the test function did to guess *why* it didn't work and try to repair that. Two example such passes are ``zero_examples`` and the various passes that try to minimize individual blocks lexicographically. What happens in ``zero_examples`` is that we try replacing the region corresponding to a draw with all zero bytes. If that doesn't work, we check if that was because of changing the size of the example (e.g. doing that with a list will make the list much shorter) and messing up the byte stream after that point. If this was what happened then we try again with a sequence of zeroes that corresponds to the size of the draw call in the version we tried that didn't work. The logic for what we do with block minimization is in ``try_shrinking_blocks``. When it tries shrinking a block and it doesn't work, it checks if the sized changed. If it does then it tries deleting the number of bytes that were lost immediately after the shrunk block to see if it helps. -------------- Playing Around -------------- I often find that it is informative to watch the shrink process in action using Hypothesis's verbosity settings. This can give you an idea of what the format of your data is, and how the shrink process transforms it. In particular, it is often useful to run a test with the flag ``-s`` to tell it not to hide output and the environment variable ``HYPOTHESIS_VERBOSITY_LEVEL=debug``. This will give you a very detailed log of what the testing process is running, along with information about what passes in the shrinker rare running and how they transform it. --------------- Getting Started --------------- The best way of getting started on working on the engine is to work on the shrinker. This is because it has the most well defined problems, the best documented code among the engine, and it's generally fun to work on. If you have not already done so, check out `Issue #1093 `_, which collates a number of other issues about shrink quality that are good starting points for people. The best place to get started thus is to take a look at those linked issues and jump in and try things! Find one that you think sounds fun. Note that some of them suggest not doing these as your first foray into the shrinker, as some are harder than others. *Please* ask questions if you have any - either the main issue for general purpose questions or specific issues for questions about a particular problem - if you get stuck or if anything doesn't make sense. We're trying to make this process easier for everyone to work on, so asking us questions is actively helpful to us and we will be very grateful to you for doing so. ================================================ FILE: guides/review.rst ================================================ =================================== The Hypothesis Code Review Handbook =================================== This document outlines the process for reviewing changes to Hypothesis. It's partly descriptive, partly prescriptive, and entirely prone to change in response to circumstance and need. We're still figuring this thing out! ----------------- What Needs Review ----------------- All changes must be signed off by at least one person with write access to the repo other than the author of the change. ---------------- How Review Works ---------------- Once the build is green and a reviewer has approved the change, anyone on the maintainer team may merge the request. More than one maintainer *may* review a change if they wish to, but it's not required. Any maintainer may block a pull request by requesting changes. Consensus on a review is best but not required. If some reviewers have approved a pull request and some have requested changes, ideally you would try to address all of the changes, but it is OK to dismiss dissenting reviews if you feel it appropriate. We've not tested the case of differing opinions much in practice yet, so we may grow firmer guidelines on what to do there over time. ------------ Review Goals ------------ At a high level, the two things we're looking for in review are answers to the following questions: 1. Is this change going to make users' lives worse? 2. Is this change going to make the maintainers' lives worse? Code review is a collaborative process between the author and the reviewer to try to ensure that the answer to both of those questions is no. Ideally of course the change should also make one or both of the users' and our lives *better*, but it's OK for changes to be mostly neutral. The author should be presumed to have a good reason for submitting the change in the first place, so neutral is good enough! -------------- Social Factors -------------- * Always thank external contributors. Thank maintainers too, ideally! * Remember that the `Code of Conduct `_ applies to pull requests and issues too. Feel free to throw your weight around to enforce this if necessary. * Anyone, maintainer or not, is welcome to do a code review. Only official maintainers have the ability to actually approve and merge a pull request, but outside review is also welcome. ------------ Requirements ------------ The rest of this document outlines specific things reviewers should focus on in aid of this, broken up by sections according to their area of applicability. All of these conditions must be satisfied for merge. Where the reviewer thinks this conflicts with the above higher level goals, they may make an exception if both the author and another maintainer agree. ~~~~~~~~~~~~~ Orthogonality ~~~~~~~~~~~~~ For all minor or patch releases, we enforce a hard and fast rule that they contain no more than one user-visible change. Major releases are allowed to bundle multiple changes together, but these should be structured as smaller pull requests into some tracking branch. We are currently very bad at this, so reviewers should feel empowered to be extra strict and provide a lot of push back on this. What counts as a user visible change is somewhat up to individual judgement, but you should err in the direction of assuming that if it might count then it does count. A good rule of thumb is that if the ``RELEASE.rst`` uses the words "additionally" or needs bullet points to be clear, it is likely too large. Ideally changes that are not user visible should also be self-contained into their own releases, but a certain amount of leniency is permitted - it's certainly OK to do a moderate amount of refactoring while you're in the area, and if a pull request involves no release at all then the same level of orthogonality is not required (but is still desirable). ~~~~~~~~~~~~~~~~~~~~~~ Clarity of Description ~~~~~~~~~~~~~~~~~~~~~~ The ``RELEASE.rst`` should contain a description of the change that makes clear: 1. The motivation for the change 2. The likely consequences of the change This doesn't have to be an essay. If you're following the orthogonality requirements a paragraph or two is likely sufficient. Any additional information that is useful to reviewers should be provided in the pull request comment. This can include e.g. background, why the particular approach was taken, references to internals that are unlikely to be of interest to users. ~~~~~~~~~~~~~~~~~~~~~ Functionality Changes ~~~~~~~~~~~~~~~~~~~~~ This section applies to any changes in Hypothesis's behaviour, regardless of their nature. A good rule of thumb is that if it touches a file in src then it counts. 1. The code should be clear in its intent and behaviour. 2. Behaviour changes should come with appropriate tests to demonstrate the new behaviour. 3. Hypothesis must never be *flaky*. Flakiness here is defined as anything where a test fails and this does not indicate a bug in Hypothesis or in the way the user wrote the code or the test. 4. The changelog (in ``RELEASE.rst``) should bump the minor or patch version (see guides/documentation.rst for details), accurately describe the changes, and shouldn't refer to internal-only APIs. For complicated markup, consider building the docs and manually checking the changelog for formatting errors that didn't result in a compilation error. ~~~~~~~~~~~ API Changes ~~~~~~~~~~~ Public API changes require the most careful scrutiny of all reviews, because they are the ones we are stuck with for the longest: Hypothesis follows semantic versioning, and we don't release new major versions very often. Public API changes must satisfy the following: 1. All public API changes must be well documented. If it's not documented, it doesn't count as public API! 2. Changes must be backwards compatible. Where this is not possible, they must first introduce a deprecation warning, then once the major version is bumped the deprecation warning and the functionality may be removed. 3. If an API is deprecated, the deprecation warning must make it clear how the user should modify their code to adapt to this change ( possibly by referring to documentation). If the required code change could be automated, the deprecation should have either `a codemod to fix it `__ or a tracking issue to write one (see "asking for more work" below). 4. If it is likely that we will want to make backwards incompatible changes to an API later, to whatever extent possible these should be made immediately when it is introduced instead. 5. APIs should give clear and helpful error messages in response to invalid inputs. In particular error messages should always display the value that triggered the error, and ideally be specific about the relevant feature of it that caused this failure (e.g. the type). 6. Incorrect usage should never "fail silently" - when a user accidentally misuses an API this should result in an explicit error. 7. Functionality should be limited to that which is easy to support in the long-term. In particular functionality which is very tied to the current Hypothesis internals should be avoided. 8. `DRMacIver `_ or `Zac-HD `_ must approve the changes though other maintainers are welcome and likely to chip in to review as well. 9. We have a separate guide for `house API style `_ which should be followed. ~~~~~~~~~ Bug Fixes ~~~~~~~~~ 1. All bug fixes must come with a test that demonstrates the bug on master and which is fixed in this branch. An exception *may* be made here if the submitter can convincingly argue that testing this would be prohibitively difficult. 2. Where possible, a fix that makes it impossible for similar bugs to occur is better. 3. Where possible, a test that will catch both this bug and a more general class of bug that contains it is better. ~~~~~~~~~~~~~~~~ Settings Changes ~~~~~~~~~~~~~~~~ Note: This section currently only applies to the Python version. It is tempting to use the Hypothesis settings object as a dumping ground for anything and everything that you can think of to control Hypothesis. This rapidly gets confusing for users and should be carefully avoided. New settings should: 1. Be something that the user can meaningfully have an opinion on. Many of the settings that have been added to Hypothesis are just cases where Hypothesis is abdicating responsibility to do the right thing to the user. 2. Make sense without reference to Hypothesis internals. 3. Correspond to behaviour which can meaningfully differ between tests - either between two different tests or between two different runs of the same test (e.g. one use case is the profile system, where you might want to run Hypothesis differently in CI and development). If you would never expect a test suite to have more than one value for a setting across any of its runs, it should be some sort of global configuration, not a setting. When deprecating a setting for later removal, we prefer to change the default value of the setting to a private singleton (``not_set``), and implement the future behaviour immediately. Passing any other value triggers a deprecation warning, but is otherwise a no-op (i.e. we still use the future behaviour). For settings where this would be especially disruptive, we have also prefixed that deprecation process with a process where we emit a warning, add a special value that can be passed to opt-in to the future behaviour, and then in the following major release we deprecate *that*, make it an no-op, and make it an error to pass any other value. ~~~~~~~~~~~~~~ Engine Changes ~~~~~~~~~~~~~~ Engine changes are anything that change a "fundamental" of how Hypothesis works. A good rule of thumb is that an engine change is anything that touches a file in ``hypothesis.internal.conjecture`` (Python version). All such changes should: 1. Be approved (or authored) by DRMacIver or Zac-HD. 2. Be approved (or authored) by someone who *isn't* DRMacIver (a major problem with this section of the code is that there is too much that only DRMacIver understands properly and we want to fix this). 3. If appropriate, come with a test in test_discovery_ability.py showing new examples that were previously hard to discover. 4. If appropriate, come with a test in test_shrink_quality.py showing how they improve the shrinker. ~~~~~~~~~~~~~~~~~~~~~~ Non-Blocking Questions ~~~~~~~~~~~~~~~~~~~~~~ These questions should *not* block merge, but may result in additional issues or changes being opened, either by the original author or by the reviewer. 1. Is this change well covered by the review items and is there anything that could usefully be added to the guidelines to improve that? 2. Were any of the review items confusing or annoying when reviewing this change? Could they be improved? 3. Are there any more general changes suggested by this, and do they have appropriate issues and/or pull requests associated with them? ~~~~~~~~~~~~~~~~~~~~ Asking for more work ~~~~~~~~~~~~~~~~~~~~ Reviewers should in general not request changes that expand the scope of a pull request beyond its original intended goal. The primary design philosophy of our work-flow is that making correct changes should be cheap, and scope creep on pull requests works against that - If you can't touch something without having to touch a number of related areas as well, changing things becomes expensive again. This of course doesn't cover things where additional work is required to ensure the change is actually correct - for example, if you change public functionality you certainly need to update its documentation. That isn't scope creep, that's just the normal scope. If a pull request suggests additional work then between the reviewer and the author people should ensure that there are relevant tracking issues for that work (as per question 3 in "Non-Blocking Questions" above), but there is no obligation for either of them to actually do any of the work on those issues. By default it is the reviewer who should open these issues, but the author is welcome to as well. That being said, it's legitimate to expand the scope of a pull request in some cases. For example: * If not doing so is likely to cause problems later. For example, because of backwards compatibility requirements it might make sense to ask for some additional functionality that is likely to be added later so that the arguments to a function are in a more sensible order. * Cases where the added functionality feels extremely incomplete in some way without an additional change. The litmus test here should be "this will almost never be useful because...". This is still fairly subjective, but at least one good use case where the change is a clear improvement over the status quo is enough to indicate that this doesn't apply. If it's unclear, the reviewer should feel free to suggest additional work (but if the author is someone new, please make sure that it's clear that this is a suggestion and not a requirement!), but the author of the pull request should feel equally free to decline the suggestion. ================================================ FILE: guides/strategies-that-shrink.rst ================================================ =================================== Designing strategies to shrink well =================================== Reducing test cases to a minimal example is a great feature of Hypothesis, the implementation of which depends on both the shrinking engine and the structure of the strategy (or combination of strategies) which created the example to reduce. This document is organised into three parts: 1. How to tell if you need to think about shrinking (you probably don't!) 2. Designing for shrinking 'above' the Hypothesis public API 3. Implementation tricks used in our internals, for interested contributors It is written for people implementing complex third-party strategies (such as `hypothesis-networkx `__), current or potential contributors to Hypothesis itself, and anyone interested in how this works under the hood. ------------------------------------ Do you need to design for shrinking? ------------------------------------ You should only attempt to tune custom strategies for better shrinking behaviour if more time would otherwise be spent reducing examples by hand or debugging more complex examples. It *may* be worthwhile if: - Your custom strategy will be used by many people, so that spending the same effort tuning the strategy has much larger benefits, or - You have personally spent time debugging failures which better example shrinking could have avoided and think this might happen again. If neither of these apply to you, relax! Hypothesis' test-case reduction is among the best in the world, and our built-in strategies are carefully designed to work well with it as discussed below. ------------------------------------ Shrinking for third-party strategies ------------------------------------ That is, strategies built out of other strategies until you get down to Hypothesis' public API. These often but not always use ``@composite``. Composition of shrinking ~~~~~~~~~~~~~~~~~~~~~~~~ The first and most important rule is that Hypothesis shrinks from the 'bottom up'. If any component of your strategy is replaced with a simpler example, the end result should also become simpler. We usually try to define "simpler" here to match a reasonable intuition about the strategy, and avoid weird edge cases when it's combined with another strategy or predicate. `Issue #1076 `_, where magnitude constraints were added to the ``complex_numbers`` strategy, makes a nice case study. We wanted to continue shrinking the real and imaginary parts like ``builds(complex, floats(), floats())``. In a worst-case scenario, the performance of filtering could be arbitrarily bad, while a 'generate and scale' approach would mean that simple inputs could lead to irrational outputs. Instead, we choose an imaginary part between +/- max_magnitude, then calculate the resulting bounds on the real part and draw it from a strategy that will always be valid. This ensures that the imaginary part shrinks to zero first, as we think real-valued complex numbers are simpler than imaginary-valued complex numbers. Let generation be lucky ~~~~~~~~~~~~~~~~~~~~~~~ Sometimes, it's worth searching for a particularly nasty value to try. This trick should be used sparingly, and always behind a branch that the shrinker can decide not to take such as ``if draw(booleans()):``, but might occasionally worth trying. Measure the results before you keep it! `Issue #69 `_ provides a nice case study: when generating tz-aware datetimes, we would like to generate instants that are skipped or repeated due to a daylight-savings transition more often than by chance. Of course, there may or may not be any such moments allowed by the bounds and tz strategy! Eliding much of the detail, a key part is to find such a moment between two endpoints, when we can only check whether one or more exists. The traditional approach would be to use a binary search, but this would be relatively expensive to shrink as we would pay the log-n cost on every attempted shrink. Instead of choosing the midpoint, we draw a *random* point between our known endpoints, and repeat this until we find a satisfactory moment. This allows the shrinker to delete all the intermediate draws - and appear lucky enough to find the moment we were looking for on the first guess! Keep things local ~~~~~~~~~~~~~~~~~ Hypothesis' shrinking engine sees every example as a labelled tree of choices, with possible reductions represented as operations on the tree. An attempted shrink succeeds if the new tree can be converted into an example, and the resulting example triggers the same bug in the test function. The most common way we see users breaking data locality is by drawing a size, then drawing a collection of that size. This is tempting because it's simple and it _works_, but it's often much slower than the alternatives. .. code:: python # Both of these strategies can generate exactly the same kind of examples, # but the second has better performance as well as style. integers(0, 10).flatmap(lambda n: st.lists(..., min_size=n, max_size=n)) st.lists(..., min_size=1, max_size=10) Another easy way to keep things local is to ensure that any ``.filter(...)`` or ``assume(...)`` calls you use are as close as possible to the relevant part of the strategy. That way, Hypothesis can retry just the part that failed instead of the entire strategy, which might be much slower. For efficient shrinking, local operations on the tree should correspond with valid (and preferably local) shrinks to the final example. For example: .. code:: python # This form of loop is hard to shrink, because we'd have to reduce `n` and # delete something in the loop simultaneously. It's equivalent to the # `.flatmap` example above. We _do_ shrink this, but much more slowly. n = draw(integers(0, 10)) for _ in range(n): ... draw(...) ... # In this form, the shrinker can see a repeated structure of labels # and delete one loop iteration without touching anything else. # We use a variant of this trick to generate collections internally! while draw(integers(0, x)) > threshold: ... draw(...) ... Similarly, it's better to draw all the attributes or inputs you need for an object at the same time, again so they can be modified or deleted together. The exact behaviour of the shrinking is a topic of active research and development, so if you are interested in the details we recommend reading the `internals guide `_ and the well-commented source code in ``hypothesis.internal.conjecture`` as well as David's ECOOP 2020 paper `Test-Case Reduction via Test-Case Generation: Insights From the Hypothesis Reducer `__. ------------------------------------- Shrinking in the Hypothesis internals ------------------------------------- The last section is for current or prospective Hypothesis contributors only. These tricks rely on implementation details that are not available to third-party libraries or users, **and can change in any patch release**. Occasionally they are also indispensable to get good performance in underlying primitives, so please contact us if the public API is not enough and we may be able to work something out. What do internals get you? ~~~~~~~~~~~~~~~~~~~~~~~~~~ Using the low-level, internal APIs complements, rather than changing, the principles above. The bytestream-level view has some important advantages: Because we operate at the level of bits, the relationship between a value and the corresponding buffer is much more obvious. If we're careful, that means we can calculate the value we want and then write the corresponding buffer to recreate it when the test case is shrunk or replayed. A small step up from bits, we can also see the spans that indicate a subset of the buffer to consider for various transformations such as transposition or deletion. Sometimes these features are the only way to maintain acceptable performance in very rare or even pathological cases - consider shrinking a complex number with a single allowed magnitude - but it's almost certain that someone will need the core strategies to do just that. However, using low-level APIs also comes at a cost - they are verbose and generally more difficult to use, and can violate key invariants of the engine if misused. Internally, our strategies mostly use the public API or something that looks a lot like ``@composite``, so it's fairly easy to follow along. There are just a few tricks enabled by those low-level advantages that we wanted to name and document, so we can recognise them discuss them and invent more... Make your own luck ~~~~~~~~~~~~~~~~~~ This is the simplest trick that uses our ability to write choices to the buffer. We use it for ``sampled_from(...).filter(...)``, after trying an initial draw with the usual rejection sampling technique, and added the ``SearchStrategy.do_filtered_draw`` method so other strategies can opt-in as we design similar tricks for their structure. It was originally designed for stateful testing, where "lucky generation" might be inefficient if there are many rules but only a few allowed by their preconditions. Here's how it works for stateful testing: 1. Draw an index into the unfiltered list of rules. Return the corresponding rule if it's allowed - we got lucky! (or someone set us up...) 2. Create a list of allowed rules, and choose one from that shortlist instead. 3. Find the index of the chosen rule *in the unfiltered list*, and write that index to the buffer. Finally, return the chosen rule. When the shrinker tries to delete the first two draws, the resulting buffer will lead to the same rule being chosen at step *one* instead. We've made our own luck! This trick is especially useful when we want to avoid rejection sampling (the ``.filter`` method, ``assume``) for performance reasons, but also need to give the shrinker the same low-level representation for each instance of a repeated choice. Flags "shrink open" ~~~~~~~~~~~~~~~~~~~ An important insight from `Swarm Testing (PDF) `__ is that randomly disabling some features can actually reduce the expected time before finding a bug, because some bugs may be suppressed by otherwise common features or attributes of the data. As discussed on `issue #1401 `__, there are a few points to keep in mind when implementing shrinkable swarm testing: - You need swarm flags to "shrink open" so that once the shrinker has run to completion, all flags are enabled. e.g. you could do this by generating a set of banned flags. - You need to use rejection sampling rather than anything more clever, or at least look like it to the shrinker. (see e.g. *Make your own luck*, above) Taking Unicode as an example, we'd like to use our knowledge of Unicode categories to generate more complex examples, but shrink the generated string without reference to categories. While we haven't actually implemented this yet - it's pretty hairy - the simple version of the idea goes like this: 1. Generate a set of banned categories. 2. Use ``characters().filter(category_is_not_banned)`` When shrinking, we start by removing categories from the banned set, after which characters in the string can be reduced as usual. In a serious version, the make-your-own-luck approach would be essential to make the filter reasonably efficient, but that's not a problem internally. In more complicated structures, it would be nice to generate the flags on first use rather than up front before we know if we need them. The trick there is to write each flag to the buffer every time we check it, in such a way that if we delete the first use the second turns into an initialisation. Explicit example boundaries ~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is almost always handled implicitly, e.g. by ``cu.many``, but *sometimes* it can be useful to explicitly insert boundaries around draws that should be deleted simultaneously using ``data.start_span``. This is used to group the value and sign of floating-point numbers, for example, which we split up in order to provide a more natural shrinking order. Explicit example management can also be useful to delineate variably-sized draws, such as our internal helper ``cu.biased_coin``, which makes eliminating dead bytes much cheaper. Finally, labelling otherwise indistinguishable draws means the shrinker can attempt to swap only the like values. ================================================ FILE: guides/testing-hypothesis.rst ================================================ ================== Testing Hypothesis ================== Note: This guide is currently entirely specific to the Python version of Hypothesis. This is a guide to the process of testing Hypothesis itself, both how to run its tests and how to write new ones. -------------------------- General Testing Philosophy -------------------------- The test suite for Hypothesis is unusually powerful - as you might hope! - but the secret is actually more about attitude than technology. The key is that we treat any bug in Hypothesis as a bug in our test suite too - and think about the kinds of bugs that might not be caught, then write tests that would catch them. We also use a variety of tools to check our code automatically, including formatting, import order, linting, and typing our API with Mypy. All of this is checked in CI - which means that once the build is green, humans can all focus on meaningful review rather than nitpicking operator spacing. Similarly, we require all code to have tests with 100% branch coverage - as a starting point, not the final goal. - Requiring full coverage can't guarantee that we've written all the tests worth writing (for example, maybe we left off a useful assertion about the result), but less than full coverage guarantees that there's some code we're not testing at all. - Tests beyond full coverage generally aim to demonstrate that a particular feature works, or that some subtle failure case is not present - often because when it was found and fixed, someone wrote a test to make sure it couldn't come back! The ``hypothesis-python/tests/`` directory has some notes in the README file on where various kinds of tests can be found or added. Go there for the practical stuff, or just ask one of the maintainers for help on a pull request! Further reading: How `SQLite is tested `_, `how the Space Shuttle was tested `_, `how to misuse code coverage `_ (for inspiration, *not* implementation). Dan Luu writes about `fuzz testing `_ and `broken processes `_, among other things. ------------- Running Tests ------------- Tests are run via ``build.sh``. See ``CONTRIBUTING.rst`` for more details. ================================================ FILE: hypothesis-python/LICENSE.txt ================================================ Copyright (c) 2013, David R. MacIver All code in this repository except where explicitly noted otherwise is released under the Mozilla Public License v 2.0. You can obtain a copy at https://mozilla.org/MPL/2.0/. Some code in this repository comes from other projects. Where applicable, the original copyright and license are noted and any modifications made are released dual licensed with the original license. Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: hypothesis-python/README.md ================================================ The Hypothesis python readme has moved to [the main readme](../README.md)! ================================================ FILE: hypothesis-python/RELEASE-sample.rst ================================================ RELEASE_TYPE: patch This patch improves import-detection in :doc:`the Ghostwriter ` (:issue:`3884`), particularly for :func:`~hypothesis.strategies.from_type` and strategies from ``hypothesis.extra.*``. Thanks to for this ! --- In the example above, "patch" on the first line should be replaced by "minor" if changes are visible in the public API, or "major" if there are breaking changes. Note that only maintainers should ever make a major release. The remaining lines are the actual changelog text for this release, which should: - concisely describe what changed _in the public API_, and why. Internal-only changes can be documented as e.g. "This release improves an internal invariant." (the complete changelog for version 6.99.11) - use ``double backticks`` for verbatim code, - use Sphinx cross-references to any functions or classes mentioned: - :pypi:`package` for links to external packages. - :func:`package.function` for link to functions, where the link text will be ``package.function``, or :func:`~package.function` to show ``function``. - :class:`package.class` for link to classes (abbreviated as above). - :issue:`issue-number` for referencing issues. - Similarly, :pull:`pr-number` can be used for PRs, but it's usually preferred to refer to version numbers with :v:`6.98.9`, as they are meaningful to end users. - :doc:`link text ` for documentation references. - `link text `__ is the same link, for general web addresses. - finish with a note of thanks from the maintainers. If this is your first contribution, don't forget to add yourself to AUTHORS.rst! After the PR is merged, the contents of this file (except the first line) are automatically added to ``docs/changelog.rst``. More examples can be found in that file. ================================================ FILE: hypothesis-python/benchmark/README.md ================================================ This directory contains plotting code for our shrinker benchmarking. The code for collecting the data is in `conftest.py`. This directory handles plotting the results. The plotting script (but not collecting benchmark data) requires additional dependencies: `pip install scipy vl-convert-python`. To run a benchmark: - `pytest tests/ -n auto --hypothesis-benchmark-shrinks new --hypothesis-benchmark-output data.json` (starting on the newer version) - `pytest tests/ -n auto --hypothesis-benchmark-shrinks old --hypothesis-benchmark-output data.json` (after switching to the old version) - Use the same `data.json` path, the benchmark will append data. You can append `-k ...` for both commands to subset the benchmark. - `python benchmark/graph.py data.json shrinking.png` This hooks any `minimal()` calls any reports the number of shrinks. Default (and currently unchangeable) number of iterations is 5 per test. ================================================ FILE: hypothesis-python/benchmark/graph.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import json import math import statistics from pathlib import Path import click def plot_vega(vega_spec, data, *, to, parameters=None): import vl_convert parameters = parameters or {} spec = json.loads(vega_spec.read_text()) spec["data"].insert(0, {"name": "source", "values": data}) if "signals" not in spec: spec["signals"] = [] for key, value in parameters.items(): spec["signals"].append({"name": key, "value": value}) with open(to, "wb") as f: # default ppi is 72, which is somewhat blurry. f.write(vl_convert.vega_to_png(spec, ppi=200)) def _mean_difference_ci(n1, n2, *, confidence): from scipy import stats var1 = statistics.variance(n1) var2 = statistics.variance(n2) df = len(n1) + len(n2) - 2 # this assumes equal variances between the populations of n1 and n2. This # is not necessarily true (new might be more consistent than old), but it's # good enough. pooled_std = math.sqrt(((len(n1) - 1) * var1 + (len(n2) - 1) * var2) / df) se = pooled_std * math.sqrt(1 / len(n1) + 1 / len(n2)) t_crit = stats.t.ppf((1 + confidence) / 2, df) return t_crit * se def _process_benchmark_data(data): assert set(data) == {"old", "new"} old_calls = data["old"]["calls"] new_calls = data["new"]["calls"] assert set(old_calls) == set(new_calls), set(old_calls).symmetric_difference( set(new_calls) ) graph_data = [] def _diff_times(old, new): if old == 0 and new == 0: return 0 if old == 0: # there aren't any great options here, but 0 is more reasonable than inf. return 0 v = (old - new) / old if 0 < v < 1: v = (1 / (1 - v)) - 1 return v sums = {"old": 0, "new": 0} for node_id in old_calls: old = old_calls[node_id] new = new_calls[node_id] if ( set(old) | set(new) == {0} or len(old) != len(new) or len(old) == len(new) == 0 ): print(f"skipping {node_id}") continue sums["old"] += statistics.mean(old) sums["new"] += statistics.mean(new) diffs = [n_old - n_new for n_old, n_new in zip(old, new, strict=True)] diffs_times = [ _diff_times(n_old, n_new) for n_old, n_new in zip(old, new, strict=True) ] ci_shrink = ( _mean_difference_ci(old, new, confidence=0.95) if len(old) > 1 else 0 ) graph_data.append( { "node_id": node_id, "absolute": statistics.mean(diffs), "absolute_ci_lower": ci_shrink, "absolute_ci_upper": ci_shrink, "nx": statistics.mean(diffs_times), "nx_ci_lower": 0, "nx_ci_upper": 0, } ) graph_data = sorted(graph_data, key=lambda d: d["absolute"]) return graph_data, sums @click.command() @click.argument("data", type=click.Path(exists=True, path_type=Path)) @click.argument("out", type=click.Path(path_type=Path)) def plot(data, out): data = json.loads(data.read_text()) data, sums = _process_benchmark_data(data) plot_vega( Path(__file__).parent / "spec.json", data=data, to=out, parameters={ "title": "Shrinking benchmark (calls)", "sum_old": sums["old"], "sum_new": sums["new"], "absolute_axis_title": ("shrink call change (old - new, larger is good)"), }, ) if __name__ == "__main__": plot() ================================================ FILE: hypothesis-python/benchmark/spec.json ================================================ { "$schema": "https://vega.github.io/schema/vega/v5.json", "width": 1600, "height": 800, "padding": 5, "background": "#ffffff", "title": { "text": {"signal": "title"}, "fontSize": 18 }, "data": [ { "name": "example", "values": [ {"node_id": "test_node_1", "absolute": 100, "absolute_ci_lower": 10, "absolute_ci_upper": 15, "nx": 0.5, "nx_ci_lower": 0.05, "nx_ci_upper": 0.07}, {"node_id": "test_node_2", "absolute": -50, "absolute_ci_lower": 5, "absolute_ci_upper": 8, "nx": -0.2, "nx_ci_lower": 0.02, "nx_ci_upper": 0.03}, {"node_id": "test_node_3", "absolute": 150, "absolute_ci_lower": 15, "absolute_ci_upper": 20, "nx": 0.6, "nx_ci_lower": 0.04, "nx_ci_upper": 0.09}, {"node_id": "test_node_4", "absolute": -120, "absolute_ci_lower": 10, "absolute_ci_upper": 12, "nx": -0.4, "nx_ci_lower": 0.03, "nx_ci_upper": 0.05}, {"node_id": "test_node_5", "absolute": 80, "absolute_ci_lower": 8, "absolute_ci_upper": 10, "nx": 0.3, "nx_ci_lower": 0.02, "nx_ci_upper": 0.04} ] }, { "name": "shrink_stats", "source": "source", "transform": [ { "type": "aggregate", "fields": ["absolute", "absolute", "nx", "nx"], "ops": ["mean", "sum", "mean", "sum"], "as": ["mean_shrink", "sum_shrink", "mean_nx", "sum_nx"] } ] }, { "name": "shrink_domain", "source": "source", "transform": [ { "type": "aggregate", "fields": ["absolute", "absolute", "absolute_ci_lower", "absolute_ci_upper"], "ops": ["min", "max", "min", "max"], "as": ["min_value", "max_value", "min_ci", "max_ci"] }, { "type": "formula", "expr": "datum.min_value - datum.min_ci", "as": "domain_min" }, { "type": "formula", "expr": "datum.max_value + datum.max_ci", "as": "domain_max" } ] }, { "name": "nx_domain", "source": "source", "transform": [ { "type": "aggregate", "fields": ["nx", "nx", "nx_ci_lower", "nx_ci_upper"], "ops": ["min", "max", "min", "max"], "as": ["min_value", "max_value", "min_ci", "max_ci"] }, { "type": "formula", "expr": "datum.min_value - datum.min_ci", "as": "domain_min" }, { "type": "formula", "expr": "datum.max_value + datum.max_ci", "as": "domain_max" } ] } ], "scales": [ { "name": "x", "type": "band", "domain": {"data": "source", "field": "node_id"}, "range": "width", "padding": 0.2 }, { "name": "y", "type": "linear", "domain": {"data": "shrink_domain", "fields": ["domain_min", "domain_max"]}, "range": "height", "nice": true, "zero": true }, { "name": "y2", "type": "linear", "domain": {"data": "nx_domain", "fields": ["domain_min", "domain_max"]}, "range": "height", "nice": true, "zero": true }, { "name": "color", "type": "ordinal", "domain": ["shrink call change", "n× change"], "range": ["#4285F4", "#DB4437"] } ], "axes": [ { "orient": "bottom", "scale": "x", "labelAngle": -90, "labelAlign": "right", "labelBaseline": "middle", "labelLimit": 300 }, { "orient": "left", "scale": "y", "title": {"signal": "absolute_axis_title"}, "titleColor": "#4285F4", "tickColor": "#4285F4", "labelColor": "#4285F4", "grid": true, "gridColor": "#e0e0e0", "gridOpacity": 0.5, "titleFontSize": 15, "tickCount": 25 }, { "orient": "right", "scale": "y2", "title": "n× change", "titleColor": "#DB4437", "tickColor": "#DB4437", "labelColor": "#DB4437", "titleFontSize": 15, "tickCount": 25 } ], "marks": [ { "type": "rule", "encode": { "enter": { "y": {"scale": "y", "value": 0}, "x": {"value": 0}, "x2": {"field": {"group": "width"}}, "stroke": {"value": "#4285F4"}, "strokeWidth": {"value": 1}, "strokeOpacity": {"value": 0.7} } } }, { "type": "rule", "encode": { "enter": { "y": {"scale": "y2", "value": 0}, "x": {"value": 0}, "x2": {"field": {"group": "width"}}, "stroke": {"value": "#DB4437"}, "strokeWidth": {"value": 1}, "strokeOpacity": {"value": 0.7} } } }, { "type": "rule", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5}, "y": {"scale": "y", "field": "absolute", "offset": {"signal": "-datum.absolute_ci_lower * height / (domain('y')[1] - domain('y')[0])"}}, "y2": {"scale": "y", "field": "absolute", "offset": {"signal": "datum.absolute_ci_upper * height / (domain('y')[1] - domain('y')[0])"}}, "stroke": {"value": "#4285F4"}, "strokeOpacity": {"value": 0.5} } } }, { "type": "rect", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5, "offset": -5}, "width": {"value": 10}, "y": {"scale": "y", "field": "absolute", "offset": {"signal": "-datum.absolute_ci_lower * height / (domain('y')[1] - domain('y')[0])"}}, "height": {"value": 1}, "fill": {"value": "#4285F4"}, "fillOpacity": {"value": 0.5} } } }, { "type": "rect", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5, "offset": -5}, "width": {"value": 10}, "y": {"scale": "y", "field": "absolute", "offset": {"signal": "datum.absolute_ci_upper * height / (domain('y')[1] - domain('y')[0])"}}, "height": {"value": 1}, "fill": {"value": "#4285F4"}, "fillOpacity": {"value": 0.5} } } }, { "type": "symbol", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5}, "y": {"scale": "y", "field": "absolute"}, "fill": {"value": "#4285F4"}, "size": {"value": 100}, "opacity": {"value": 0.35} } } }, { "type": "symbol", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5}, "y": {"scale": "y", "field": "absolute"}, "fill": {"value": "#4285F4"}, "size": {"value": 38}, "opacity": {"value": 0.85} } } }, { "type": "rule", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5}, "y": {"scale": "y2", "field": "nx", "offset": {"signal": "-datum.nx_ci_lower * height / (domain('y2')[1] - domain('y2')[0])"}}, "y2": {"scale": "y2", "field": "nx", "offset": {"signal": "datum.nx_ci_upper * height / (domain('y2')[1] - domain('y2')[0])"}}, "stroke": {"value": "#DB4437"}, "strokeOpacity": {"value": 0.5} } } }, { "type": "rect", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5, "offset": -5}, "width": {"value": 10}, "y": {"scale": "y2", "field": "nx", "offset": {"signal": "-datum.nx_ci_lower * height / (domain('y2')[1] - domain('y2')[0])"}}, "height": {"value": 1}, "fill": {"value": "#DB4437"}, "fillOpacity": {"value": 0.5} } } }, { "type": "rect", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5, "offset": -5}, "width": {"value": 10}, "y": {"scale": "y2", "field": "nx", "offset": {"signal": "datum.nx_ci_upper * height / (domain('y2')[1] - domain('y2')[0])"}}, "height": {"value": 1}, "fill": {"value": "#DB4437"}, "fillOpacity": {"value": 0.5} } } }, { "type": "symbol", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5}, "y": {"scale": "y2", "field": "nx"}, "fill": {"value": "#DB4437"}, "size": {"value": 100}, "opacity": {"value": 0.35} } } }, { "type": "symbol", "from": {"data": "source"}, "encode": { "enter": { "x": {"scale": "x", "field": "node_id", "band": 0.5}, "y": {"scale": "y2", "field": "nx"}, "fill": {"value": "#DB4437"}, "size": {"value": 38}, "opacity": {"value": 0.85} } } }, { "type": "group", "encode": { "enter": { "x": {"value": 1630}, "y": {"value": 20}, "width": {"value": 140}, "height": {"value": 130}, "cornerRadius": {"value": 5}, "fill": {"value": "#f0f0f0"}, "stroke": {"value": "#cccccc"} } }, "marks": [ { "type": "text", "from": {"data": "shrink_stats"}, "encode": { "enter": { "x": {"value": 10}, "y": {"value": 20}, "text": {"signal": "'Mean: ' + format(datum.mean_shrink, ',.1f')"}, "fontSize": {"value": 14}, "fill": {"value": "#4285F4"} } } }, { "type": "text", "from": {"data": "shrink_stats"}, "encode": { "enter": { "x": {"value": 10}, "y": {"value": 40}, "text": {"signal": "'Sum: ' + format(datum.sum_shrink, ',d')"}, "fontSize": {"value": 14}, "fill": {"value": "#4285F4"} } } }, { "type": "text", "from": {"data": "shrink_stats"}, "encode": { "enter": { "x": {"value": 10}, "y": {"value": 60}, "text": {"signal": "'Mean: ' + format(datum.mean_nx, ',.1f')"}, "fontSize": {"value": 14}, "fill": {"value": "#DB4437"} } } }, { "type": "text", "encode": { "enter": { "x": {"value": 10}, "y": {"value": 80}, "text": {"signal": "'sum(old): ' + format(sum_old, ',.1f')"}, "fontSize": {"value": 14}, "fill": {"value": "#505050"} } } }, { "type": "text", "encode": { "enter": { "x": {"value": 10}, "y": {"value": 100}, "text": {"signal": "'sum(new): ' + format(sum_new, ',.1f')"}, "fontSize": {"value": 14}, "fill": {"value": "#505050"} } } }, { "type": "text", "encode": { "enter": { "x": {"value": 10}, "y": {"value": 120}, "text": {"signal": "'old / new: ' + format(sum_old / sum_new, ',.2f')"}, "fontSize": {"value": 14}, "fill": {"value": "#505050"} } } } ] } ] } ================================================ FILE: hypothesis-python/docs/_ext/hypothesis_linkcheck.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sphinx from sphinx.application import Sphinx from sphinx.builders.linkcheck import HyperlinkAvailabilityChecker # We want to customize the linkcheck behavior so that references from intersphinx # mappings are not checked. We use these liberally and don't want to spend CI time # checking their validity. If it's in an inventory, sphinx should guarantee # it's valid, sans very weird things happening. # # Sphinx splits the link check logic across a CheckExternalLinksBuilder builder # and a HyperlinkCollector post_transform (and a HyperlinkAvailabilityChecker # helper class). There are various points in each where we could add this # ignore-intersphinx hook. # # Monkey-patching HyperlinkAvailabilityChecker isn't great, but is the best way # I found to go about this. # set by on_builder_inited inventories = {} def is_intersphinx_link(uri): for inventory in inventories.values(): uris = {uri for _name, _version, uri, _display_name in inventory.values()} if uri in uris: return True return False class HypothesisLinkChecker(HyperlinkAvailabilityChecker): def is_ignored_uri(self, uri: str) -> bool: if is_intersphinx_link(uri): return True return super().is_ignored_uri(uri) sphinx.builders.linkcheck.HyperlinkAvailabilityChecker = HypothesisLinkChecker # Hook the builder to get access to the intersphinx inventory. app.env is not # available in setup() def on_builder_inited(app: Sphinx) -> None: global inventories inventories = getattr(app.env, "intersphinx_inventory", {}) def setup(app: Sphinx): app.connect("builder-inited", on_builder_inited) ================================================ FILE: hypothesis-python/docs/_ext/hypothesis_redirects.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. # vendored from https://github.com/documatt/sphinx-reredirects under the MIT # license, with thanks # ruff: noqa: G004 import re from collections.abc import Mapping, Sequence from fnmatch import fnmatch from pathlib import Path from string import Template from urllib.parse import urlparse from sphinx.application import Sphinx from sphinx.builders.linkcheck import CheckExternalLinksBuilder, Hyperlink from sphinx.util import logging from sphinx.util.osutil import SEP OPTION_REDIRECTS = "redirects" OPTION_REDIRECTS_DEFAULT: dict[str, str] = {} OPTION_TEMPLATE_FILE = "redirect_html_template_file" OPTION_TEMPLATE_FILE_DEFAULT = None REDIRECT_FILE_DEFAULT_TEMPLATE = ( '' ) logger = logging.getLogger(__name__) wildcard_pattern = re.compile(r"[\*\?\[\]]") def setup(app: Sphinx) -> dict: """ Extension setup, called by Sphinx """ app.connect("html-collect-pages", init) app.connect("builder-inited", collect_redirects_for_linkcheck) app.add_config_value(OPTION_REDIRECTS, OPTION_REDIRECTS_DEFAULT, "env") app.add_config_value(OPTION_TEMPLATE_FILE, OPTION_TEMPLATE_FILE_DEFAULT, "env") return {"parallel_read_safe": True} def init(app: Sphinx) -> Sequence | None: if not app.config[OPTION_REDIRECTS]: logger.debug("No redirects configured") return [] rr = Reredirects(app) to_be_redirected = rr.grab_redirects() rr.create_redirects(to_be_redirected) # html-collect-pages requires to return iterable of pages to write, # we have no additional pages to write return [] class Reredirects: def __init__(self, app: Sphinx) -> None: self.app = app self.redirects_option: dict[str, str] = getattr(app.config, OPTION_REDIRECTS) self.template_file_option: str = getattr(app.config, OPTION_TEMPLATE_FILE) def grab_redirects(self) -> Mapping[str, str]: """Inspect redirects option in conf.py and returns dict mapping docname to target (with expanded placeholder).""" # docname-target dict to_be_redirected = {} # For each source-target redirect pair in conf.py for source, target in self.redirects_option.items(): # no wildcard, append source as-is if not self._contains_wildcard(source): to_be_redirected[source] = target continue assert self.app.env # wildcarded source, expand to docnames expanded_docs = [ doc for doc in self.app.env.found_docs if fnmatch(doc, source) ] if not expanded_docs: logger.warning(f"No documents match to '{source}' redirect.") continue for doc in expanded_docs: new_target = self._apply_placeholders(doc, target) to_be_redirected[doc] = new_target return to_be_redirected def docname_out_path(self, docname: str, suffix: str) -> Sequence[str]: """ For a Sphinx docname (the path to a source document without suffix), returns path to outfile that would be created by the used builder. """ # Return as-is, if the docname already has been passed with a suffix if docname.endswith(suffix): return [docname] # Remove any trailing slashes, except for "/"" index if len(docname) > 1 and docname.endswith(SEP): docname = docname.rstrip(SEP) # Figure out whether we have dirhtml builder out_uri = self.app.builder.get_target_uri(docname=docname) # type: ignore if not out_uri.endswith(suffix): # If dirhtml builder is used, need to append "index" return [out_uri, "index"] # Otherwise, convert e.g. 'source' to 'source.html' return [out_uri] def create_redirects(self, to_be_redirected: Mapping[str, str]) -> None: """Create actual redirect file for each pair in passed mapping of docnames to targets.""" # Corresponds to value of `html_file_suffix`, but takes into account # modifications done by the builder class try: suffix = self.app.builder.out_suffix # type: ignore except Exception: suffix = ".html" for docname, target in to_be_redirected.items(): out = self.docname_out_path(docname, suffix) redirect_file_abs = Path(self.app.outdir).joinpath(*out).with_suffix(suffix) redirect_file_rel = redirect_file_abs.relative_to(self.app.outdir) if redirect_file_abs.exists(): logger.info( f"Overwriting '{redirect_file_rel}' with redirect to '{target}'." ) else: logger.info(f"Creating redirect '{redirect_file_rel}' to '{target}'.") self._create_redirect_file(redirect_file_abs, target) @staticmethod def _contains_wildcard(text: str) -> bool: """Tells whether passed argument contains wildcard characters.""" return bool(wildcard_pattern.search(text)) @staticmethod def _apply_placeholders(source: str, target: str) -> str: """Expand "source" placeholder in target and return it""" return Template(target).substitute({"source": source}) def _create_redirect_file(self, at_path: Path, to_uri: str) -> None: """Actually create a redirect file according to redirect template""" content = self._render_redirect_template(to_uri) # create any missing parent folders at_path.parent.mkdir(parents=True, exist_ok=True) at_path.write_text(content, encoding="utf-8") def _render_redirect_template(self, to_uri: str) -> str: # HTML used as redirect file content redirect_template = REDIRECT_FILE_DEFAULT_TEMPLATE if self.template_file_option: redirect_file_abs = Path(self.app.srcdir, self.template_file_option) redirect_template = redirect_file_abs.read_text(encoding="utf-8") return Template(redirect_template).substitute({"to_uri": to_uri}) def collect_redirects_for_linkcheck(app): # Ignore when not invoked with linkcheck builder if not isinstance(app.builder, CheckExternalLinksBuilder): return redirects = Reredirects(app).grab_redirects() for docname, target in redirects.items(): # Give a Sphinx or extensions change to modify original target URL if new_target := app.emit_firstresult("linkcheck-process-uri", target): target = new_target if urlparse(target).scheme not in ("http", "https"): # Checking redirects to other pages of the same documentation is not # supported for now. continue # Add target external URL to hyperlinks which linkcheck builder will check docpath = app.env.doc2path(docname) hyperlink = Hyperlink(uri=target, docname=docname, docpath=docpath, lineno=-1) app.builder.hyperlinks[target] = hyperlink ================================================ FILE: hypothesis-python/docs/_static/better-signatures.css ================================================ /* dl gets used both for defining each top-level `.. autofunc` on a page (where we want vertical margsin) and is wrapped around multiline signatures (where we don't). If a dl is being used inside a .sig, that's a multiline signature; remove its margins. */ .sig > dl { margin-block-start: 0rem; margin-block-end: 0rem; } /* with thanks to https://github.com/pradyunsg/furo/discussions/749 */ .sig:not(.sig-inline) { padding-left: 0.5em; text-indent: 0; } ================================================ FILE: hypothesis-python/docs/_static/dark-fix.css ================================================ /* See https://github.com/HypothesisWorks/hypothesis/issues/4588 and https://github.com/pradyunsg/furo/discussions/909. Once this is fixed in furo, we can remove this. */ body[data-theme="dark"] .tooltip .tooltip-content { background-color: var(--color-background-primary); } body[data-theme="dark"] .tooltip .arrow { background: var(--color-background-primary); } ================================================ FILE: hypothesis-python/docs/_static/no-scroll.css ================================================ /* disable autoscroll-to-target behavior https://github.com/pradyunsg/furo/discussions/384#discussioncomment-2249243 */ html { scroll-behavior: auto; } ================================================ FILE: hypothesis-python/docs/_static/wrap-in-tables.css ================================================ /* override table width restrictions */ /* thanks to https://github.com/readthedocs/sphinx_rtd_theme/issues/117#issuecomment-153083280 */ @media screen and (min-width: 767px) { .wy-table-responsive table td { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } } ================================================ FILE: hypothesis-python/docs/changelog.rst ================================================ ========= Changelog ========= This is a record of all past Hypothesis releases and what went into them, in reverse chronological order. All previous releases are still available :pypi:`on PyPI `. Hypothesis 6.x ============== .. only:: has_release_file -------------------- Current pull request -------------------- .. include:: ../RELEASE.rst .. _v6.151.9: -------------------- 6.151.9 - 2026-02-16 -------------------- Remove some old unused code. .. _v6.151.8: -------------------- 6.151.8 - 2026-02-16 -------------------- This patch fixes a crash when :obj:`sys.modules` contains unhashable values, such as :class:`~types.SimpleNamespace` objects (:issue:`4660`). .. _v6.151.7: -------------------- 6.151.7 - 2026-02-16 -------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.151.6: -------------------- 6.151.6 - 2026-02-11 -------------------- This patch fixes several duplicate word typos in comments and documentation. .. _v6.151.5: -------------------- 6.151.5 - 2026-02-03 -------------------- This patch teaches our pytest plugin to :ref:` find interesting constants ` when pytest is collecting tests, to avoid arbitrarily attributing the latency to whichever test function happened to be executed first (:issue:`4627`). .. _v6.151.4: -------------------- 6.151.4 - 2026-01-29 -------------------- This patch adjusts how we compute the stopping threshold introduced in :version:`6.151.3`, while still maintaining 99% confidence that <1% of test cases pass. .. _v6.151.3: -------------------- 6.151.3 - 2026-01-28 -------------------- This patch makes Hypothesis more tolerant of slow-to-satisfy ``assume()`` calls. Previously, Hypothesis would give up after ``max_examples * 10`` attempts; now it uses a statistical test to stop only when 99% confident that <1% of examples would pass (:issue:`4623`). Thanks to @ajdavis for this improvement! .. _v6.151.2: -------------------- 6.151.2 - 2026-01-26 -------------------- Format our code with the latest version of :pypi:`black`. .. _v6.151.1: -------------------- 6.151.1 - 2026-01-26 -------------------- Improve internal categorization of test cases when an :ref:`alternative backend ` raises |BackendCannotProceed|. .. _v6.151.0: -------------------- 6.151.0 - 2026-01-25 -------------------- Add 2025.12 to the list of recognized Array API versions in ``hypothesis.extra.array_api``. .. _v6.150.3: -------------------- 6.150.3 - 2026-01-23 -------------------- Hypothesis now generates powers of 2 more often when using |st.integers|. .. _v6.150.2: -------------------- 6.150.2 - 2026-01-13 -------------------- Update some internal type hints. .. _v6.150.1: -------------------- 6.150.1 - 2026-01-12 -------------------- This patch fixes a bug where |st.recursive| would fail in cases where the ``extend=`` function does not reference it's argument - which was assumed by the recent ``min_leaves=`` feature, because the strategy can't actually recurse otherwise. (:issue:`4638`) Now, the historical behavior is working-but-deprecated, or an error if you explicitly pass ``min_leaves=``. .. _v6.150.0: -------------------- 6.150.0 - 2026-01-06 -------------------- This release adds a ``min_leaves`` argument to :func:`~hypothesis.strategies.recursive`, which ensures that generated recursive structures have at least the specified number of leaf nodes (:issue:`4205`). .. _v6.149.1: -------------------- 6.149.1 - 2026-01-05 -------------------- Add type hints to an internal class. .. _v6.149.0: -------------------- 6.149.0 - 2026-01-05 -------------------- This release extends the explain-phase ``# or any other generated value`` comments to sub-arguments within :func:`~hypothesis.strategies.builds`, :func:`~hypothesis.strategies.tuples`, and :func:`~hypothesis.strategies.fixed_dictionaries`. Previously, these comments only appeared on top-level test arguments. Now, when the explain phase determines that a sub-argument can vary freely without affecting the test failure, you'll see comments like:: Falsifying example: test_foo( obj=MyClass( x=0, # or any other generated value y=True, ), data=( '', # or any other generated value 42, ), ) This makes it easier to understand which parts of complex inputs actually matter for reproducing a failure. .. _v6.148.13: --------------------- 6.148.13 - 2026-01-05 --------------------- Clean up an internal helper. .. _v6.148.12: --------------------- 6.148.12 - 2026-01-04 --------------------- This patch fixes :func:`~hypothesis.strategies.from_type` to properly handle parameterized type aliases created with Python 3.12+'s :pep:`695` ``type`` statement. For example, ``st.from_type(A[int])`` where ``type A[T] = list[T]`` now correctly resolves to ``lists(integers())`` instead of raising a ``TypeError`` (:issue:`4628`). .. _v6.148.11: --------------------- 6.148.11 - 2026-01-03 --------------------- Hypothesis now prints a |Verbosity.verbose| log when we switch away from an :ref:`alternative backend `. .. _v6.148.10: --------------------- 6.148.10 - 2026-01-03 --------------------- Fixes :ref:`Ghostwriter ` output for :pypi:`numpy` >= 2.4.0. Also adds support |st.from_type| for :pypi:`numpy` 2.5.0 nightly (which has not yet been released). .. _v6.148.9: -------------------- 6.148.9 - 2026-01-01 -------------------- |.example| no longer emits |NonInteractiveExampleWarning| when running a python file directly. This means that e.g. ``python my_sandbox.py`` during exploratory work with |.example| will no longer raise warnings. .. _v6.148.8: -------------------- 6.148.8 - 2025-12-23 -------------------- Add ``__dict__`` and ``__proto__`` to the list of constant strings Hypothesis sometimes generates. .. _v6.148.7: -------------------- 6.148.7 - 2025-12-05 -------------------- When multiple explicit |@example| decorators fail with the same error, Hypothesis now shows only the simplest failing example (by shortlex order) with a note about how many other examples also failed (:issue:`4520`). To see all failing examples, use |Verbosity.verbose| or higher. .. _v6.148.6: -------------------- 6.148.6 - 2025-12-04 -------------------- Fix a bug where we persisted symbolics from solver-based :ref:`alternative backends ` in |event|. .. _v6.148.5: -------------------- 6.148.5 - 2025-12-01 -------------------- This patch improves the error message for :class:`~hypothesis.errors.FlakyStrategyDefinition` when the precondition for a rule is flaky (:issue:`4206`). .. _v6.148.4: -------------------- 6.148.4 - 2025-12-01 -------------------- This patch improves the type annotations for :func:`~hypothesis.extra.numpy.basic_indices`. The return type now accurately reflects the ``allow_ellipsis`` and ``allow_newaxis`` parameters, excluding ``EllipsisType`` or ``None`` from the union when those index types are disabled (:issue:`4607`). Additionally, :func:`~hypothesis.assume` now has overloaded type annotations: ``assume(True)`` returns ``Literal[True]``, while ``assume(False)`` and ``assume(None)`` return ``NoReturn``. .. _v6.148.3: -------------------- 6.148.3 - 2025-11-27 -------------------- Clean up some internal code. .. _v6.148.2: -------------------- 6.148.2 - 2025-11-18 -------------------- Document |fuzz_one_input|. .. _v6.148.1: -------------------- 6.148.1 - 2025-11-16 -------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.148.0: -------------------- 6.148.0 - 2025-11-15 -------------------- Calling :func:`~hypothesis.settings.register_profile` from within a test decorated with :func:`@settings ` is now deprecated, to avoid confusion about which settings are used as the baseline for the new profile. .. _v6.147.0: -------------------- 6.147.0 - 2025-11-06 -------------------- This release drops support for :pypi:`nose`, which ceased development 9 years ago and does not support Python 3.10 or newer. Hypothesis still supports :pypi:`nose2`. While we do not test ``nose2`` in our CI, we will fix any bugs that get reported. .. _v6.146.0: -------------------- 6.146.0 - 2025-11-05 -------------------- |@settings| now accepts equivalent string representations for |settings.verbosity|, |settings.phases|, and |settings.suppress_health_check|. For example: .. code-block:: python # these two are now equivalent... settings(verbosity=Verbosity.verbose) settings(verbosity="verbose") # ...as are these two... settings(phases=[Phase.explicit]) settings(phases=["explicit"]) # ...and these two. settings(suppress_health_check=[HealthCheck.filter_too_much]) settings(suppress_health_check=["filter_too_much"]) This release also changes the canonical value of |Verbosity|, |Phase|, and |HealthCheck| members to a string instead of an integer. For example, ``Phase.reuse.value == "explicit"`` as of this release, where previously ``Phase.reuse.value == 1``. Instantiating |Verbosity|, |Phase|, or |HealthCheck| with an integer, such as ``Verbosity(0)``, is now deprecated. .. _v6.145.1: -------------------- 6.145.1 - 2025-11-03 -------------------- Refactor some internal logic around strategy definitions. .. _v6.145.0: -------------------- 6.145.0 - 2025-11-03 -------------------- Hypothesis previously required :pypi:`attrs` as a dependency. This release removes that dependency, so that the only required dependency of Hypothesis is :pypi:`sortedcontainers`. All attrs-specific features of Hypothesis, such as using |st.from_type| with attrs classes, will continue to behave as before. .. _v6.144.1: -------------------- 6.144.1 - 2025-11-03 -------------------- Tweak how Hypothesis hides internal tracebacks to fix an error under rare conditions (:issue:`3822`). .. _v6.144.0: -------------------- 6.144.0 - 2025-11-02 -------------------- This release adds support for :class:`~fractions.Fraction` objects as ``min_value`` and ``max_value`` bounds in :func:`~hypothesis.strategies.decimals`, if they can be exactly represented as decimals in the target precision (:issue:`4466`). Bounding :func:`~hypothesis.strategies.decimals` with *other* values that cannot be exactly represented is now deprecated; previously the bounds could be off by one. .. _v6.143.1: -------------------- 6.143.1 - 2025-11-02 -------------------- :func:`~hypothesis.strategies.from_type` now correctly handles :pypi:`annotated-types` annotations on :class:`typing.TypedDict` fields which are also marked as being :obj:`~typing.ReadOnly`, :obj:`~typing.Required`, or :obj:`~typing.NotRequired` (:issue:`4474`). .. _v6.143.0: -------------------- 6.143.0 - 2025-11-01 -------------------- The extras for |hypothesis-numpy| and |hypothesis-pandas| now support automatically inferring a strategy for ``dtype="O"``. Previously, Hypothesis required an explicit elements strategy to be passed, for example ``nps.arrays("O", shape=(1,), elements=st.just(object()))``. Now, Hypothesis automatically infers ``elements=st.from_type(object)``. Thanks to Shaun Read for identifying and fixing this! .. _v6.142.5: -------------------- 6.142.5 - 2025-10-31 -------------------- This patch fixes :func:`~hypothesis.extra.ghostwriter.binary_operation` to include imports for :mod:`hypothesis.extra.numpy` strategies such as :func:`~hypothesis.extra.numpy.arrays`, :func:`~hypothesis.extra.numpy.scalar_dtypes`, and :func:`~hypothesis.extra.numpy.array_shapes` when ghostwriting tests for functions with numpy array parameters (:issue:`4576`). .. _v6.142.4: -------------------- 6.142.4 - 2025-10-25 -------------------- Improve the accuracy of test timing reports, by tracking the start time of each test case closer to when the test is executed. .. _v6.142.3: -------------------- 6.142.3 - 2025-10-22 -------------------- Fix a recursion error when :ref:`observability ` is enabled and a test generates an object with a recursive reference, like ``a = []; a.append(a)``. .. _v6.142.2: -------------------- 6.142.2 - 2025-10-20 -------------------- Remove a case where Hypothesis would interact with the global |random.Random| instance if Hypothesis internals were used directly. .. _v6.142.1: -------------------- 6.142.1 - 2025-10-16 -------------------- Simplify some internal typing logic after dropping Python 3.9. .. _v6.142.0: -------------------- 6.142.0 - 2025-10-16 -------------------- This release drops support for Python 3.9, `which reached end of life in October 2025 `__. .. _v6.141.1: -------------------- 6.141.1 - 2025-10-15 -------------------- Fixes an error when using :ref:`the Ghostwriter ` with annotations that include :obj:`python:typing.ForwardRef` on Python 3.14 (:issue:`4565`). .. _v6.141.0: -------------------- 6.141.0 - 2025-10-15 -------------------- The |django.from_field| and |django.from_form| strategies from our :ref:`Django extra ` now support :obj:`~django:django.db.models.FileField`. Thanks to Arjoonn Sharma for this fix! .. _v6.140.4: -------------------- 6.140.4 - 2025-10-14 -------------------- Clean up internal ``@overload`` type annotations. .. _v6.140.3: -------------------- 6.140.3 - 2025-10-04 -------------------- Fixes our bundled |run_conformance_test| not respecting |PrimitiveProvider.avoid_realization|. .. _v6.140.2: -------------------- 6.140.2 - 2025-09-23 -------------------- The automatic switch to the CI :class:`settings profile ` now works under :pypi:`tox` (for ``tox >= 4.30.0``). .. _v6.140.1: -------------------- 6.140.1 - 2025-09-22 -------------------- This patch re-enables the warning for incompatible :func:`~hypothesis.strategies.shared` strategies that was first enabled in :v:`6.133.0` but disabled in :v:`6.135.15`. .. _v6.140.0: -------------------- 6.140.0 - 2025-09-22 -------------------- |st.characters| now validates that the elements of the ``exclude_characters`` and ``include_characters`` arguments are single characters, which was always assumed internally. For example, ``exclude_characters=["a", "b"]`` is valid while ``exclude_characters=["ab"]`` will now raise an error up-front. .. _v6.139.3: -------------------- 6.139.3 - 2025-09-22 -------------------- Add ``phase`` to the :ref:`hypothesis-specific metadata ` in :ref:`observability `. .. _v6.139.2: -------------------- 6.139.2 - 2025-09-18 -------------------- Internal refactoring for new lint rules. .. _v6.139.1: -------------------- 6.139.1 - 2025-09-16 -------------------- Fixed another typo in error message around function-scoped fixtures. .. _v6.139.0: -------------------- 6.139.0 - 2025-09-16 -------------------- Add |settings.get_current_profile_name|, which returns the name of the current settings profile. .. _v6.138.17: --------------------- 6.138.17 - 2025-09-15 --------------------- Fixed typo in error message around function-scoped fixtures. .. _v6.138.16: --------------------- 6.138.16 - 2025-09-13 --------------------- Improved error message for |DeadlineExceeded|. .. _v6.138.15: --------------------- 6.138.15 - 2025-09-08 --------------------- Refactor some stateful testing internals for easier use by third-party libraries. .. _v6.138.14: --------------------- 6.138.14 - 2025-09-02 --------------------- Patch files written by hypothesis now use a deterministic ordering when multiple |@example| decorators are present. .. _v6.138.13: --------------------- 6.138.13 - 2025-09-01 --------------------- Fix a typo affecting pretty-printing of lambdas with complex default arguments. .. _v6.138.12: --------------------- 6.138.12 - 2025-09-01 --------------------- Improve automatic detection of the :ref:`CI profile ` on various vendor-specific CI systems. .. _v6.138.11: --------------------- 6.138.11 - 2025-09-01 --------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.138.10: --------------------- 6.138.10 - 2025-08-31 --------------------- Internal refactor to simplify |SearchStrategy|. .. _v6.138.9: -------------------- 6.138.9 - 2025-08-31 -------------------- This patch further improves stringification of lambdas, by never returning a lambda source unless it is confirmed to compile to the same code object. This stricter check makes it possible to widen the search for a matching source block, so that it can often be found even if the file has been edited. .. _v6.138.8: -------------------- 6.138.8 - 2025-08-29 -------------------- Fixes a race condition under threading when using |st.deferred|. .. _v6.138.7: -------------------- 6.138.7 - 2025-08-28 -------------------- Improves upon the cache eviction problem workaround of :v:`6.135.12`. .. _v6.138.6: -------------------- 6.138.6 - 2025-08-27 -------------------- Documentation tweaks. .. _v6.138.5: -------------------- 6.138.5 - 2025-08-27 -------------------- Fixes a race condition under threading for strategies which trigger our filter-rewriting rules, like ``st.integers().filter(lambda x: abs(x) > 100)``. .. _v6.138.4: -------------------- 6.138.4 - 2025-08-27 -------------------- One of our shrinking passes for reducing failing inputs targets failures which require two numbers to add to the same value. This pass previously only worked for positive numbers. This patch fixes that, so it also works for negative numbers. .. _v6.138.3: -------------------- 6.138.3 - 2025-08-24 -------------------- This patch slightly improves the cache-hit rate for |st.dictionaries| and certain unique |st.lists|. .. _v6.138.2: -------------------- 6.138.2 - 2025-08-16 -------------------- The type annotations for |st.register_type_strategy| now indicate that it accepts registering types created with |TypeAliasType| (aka ``type MyType = int``). .. _v6.138.1: -------------------- 6.138.1 - 2025-08-15 -------------------- Internal refactoring and cleanup. As a result, ``hypothesis[cli]`` and ``hypothesis[ghostwriter]`` now require ``black>=20.8b0`` instead of the previous ``black>=19.10b0``. .. _v6.138.0: -------------------- 6.138.0 - 2025-08-13 -------------------- On Python 3.14, |memoryview| is newly generic. This release adds the ability for |st.from_type| to resolve generic |memoryview| types on 3.14, like ``st.from_type(memoryview[CustomBufferClass])`` . ``CustomBufferClass`` must implement ``__buffer__``, as expected by |memoryview|. .. _v6.137.3: -------------------- 6.137.3 - 2025-08-11 -------------------- This patch makes the stringification of lambdas, and as a result certain automatic filter rewriting operations, more robust. This fixes :issue:`4498`, where a lambda was mistakenly identified as the identity operator due to :func:`inspect.getsource` only returning the first line of the lambda definition. As a result, the ``repr`` of strategies filtered or mapped by lambda functions may change slightly. .. _v6.137.2: -------------------- 6.137.2 - 2025-08-11 -------------------- Add support for Python 3.14, `which is currently in release candidate 1 `_. .. _v6.137.1: -------------------- 6.137.1 - 2025-08-05 -------------------- Fixes a bug with solver-based :ref:`alternative backends ` (like `crosshair `_) where symbolic values passed to |event| would not be realized to concrete values at the end of the test case. .. _v6.137.0: -------------------- 6.137.0 - 2025-08-05 -------------------- Add the |add_observability_callback|, |remove_observability_callback|, |with_observability_callback|, and |observability_enabled| methods to the :ref:`observability ` interface. The previous |TESTCASE_CALLBACKS| is deprecated. This release also adds better threading support to observability callbacks. An observability callback will now only be called for observations generated by the same thread. .. _v6.136.9: -------------------- 6.136.9 - 2025-08-04 -------------------- Fix a threading race condition in |st.one_of| initialization. .. _v6.136.8: -------------------- 6.136.8 - 2025-08-04 -------------------- Improve the error messages and documentation for |HealthCheck|. Among others, the messaging is now more clear that health checks are proactive warnings, not correctness errors. .. _v6.136.7: -------------------- 6.136.7 - 2025-08-01 -------------------- Improve detection of sys.monitoring to avoid errors on GraalPy. .. _v6.136.6: -------------------- 6.136.6 - 2025-07-28 -------------------- When a test is executed concurrently from multiple threads, |HealthCheck.too_slow| is now disabled, since the Python runtime may decide to switch away from a thread for arbitrarily long and Hypothesis cannot track execution time per-thread. .. _v6.136.5: -------------------- 6.136.5 - 2025-07-28 -------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.136.4: -------------------- 6.136.4 - 2025-07-25 -------------------- |HealthCheck.differing_executors| is no longer raised if a test is executed by different executors from different threads. |HealthCheck.differing_executors| will still be raised if a test is executed by different executors in the same thread. .. _v6.136.3: -------------------- 6.136.3 - 2025-07-23 -------------------- When a test is executed concurrently from multiple threads, |DeadlineExceeded| is now disabled, since the Python runtime may decide to switch away from a thread for longer than |settings.deadline|, and Hypothesis cannot track execution time per-thread. See :issue:`4478`. .. _v6.136.2: -------------------- 6.136.2 - 2025-07-21 -------------------- |@precondition| now errors if used without |@rule| or |@invariant|. Doing so has no effect and is indicative of a user error (:issue:`4413`). .. _v6.136.1: -------------------- 6.136.1 - 2025-07-20 -------------------- Fix |PrimitiveProvider.on_observation| being called with observations it wasn't responsible for generating if the test failed. .. _v6.136.0: -------------------- 6.136.0 - 2025-07-19 -------------------- When a failure found by an :ref:`alternative backend ` does not reproduce under the Hypothesis backend, we now raise |FlakyBackendFailure| instead of an internal ``FlakyReplay`` exception. .. _v6.135.33: --------------------- 6.135.33 - 2025-07-18 --------------------- Speculative fix for a thread-safety issue in calculating strategy labels. .. _v6.135.32: --------------------- 6.135.32 - 2025-07-15 --------------------- Improve the thread-safety of strategy validation. Before this release, Hypothesis did not require that ``super().__init__()`` be called in ``SearchStrategy`` subclasses. Subclassing ``SearchStrategy`` is not supported or part of the public API, but if you are subclassing it anyway, you will need to make sure to call ``super().__init__()`` after this release. .. _v6.135.31: --------------------- 6.135.31 - 2025-07-15 --------------------- Fix a remaining thread-safety issue with the deprecation warning for use of the global random instance (see :ref:`v6.135.24 `). .. _v6.135.30: --------------------- 6.135.30 - 2025-07-14 --------------------- Fix a remaining thread-safety issue with the recursion limit warning Hypothesis issues when an outside caller sets ``sys.setrecursionlimit`` (see :ref:`v6.135.23 `). .. _v6.135.29: --------------------- 6.135.29 - 2025-07-12 --------------------- Optimize performance of |st.sampled_from| and internal selection of :ref:`stateful testing ` rules. .. _v6.135.28: --------------------- 6.135.28 - 2025-07-12 --------------------- Optimize the memory and speed of an internal datastructure for compactly storing integers. .. _v6.135.27: --------------------- 6.135.27 - 2025-07-12 --------------------- Improve thread-safety for stateful |@initialize| rules. .. _v6.135.26: --------------------- 6.135.26 - 2025-07-05 --------------------- Fix a "dictionary changed size during iteration" error that could occur under with |register_random| under multiple threads. .. _v6.135.25: --------------------- 6.135.25 - 2025-07-05 --------------------- Improve thread safety of our :mod:`sys.monitoring` usage (by the |Phase.shrink| and |Phase.explain| phases), as well as the internal computation of strategy labels. .. _v6.135.24: --------------------- 6.135.24 - 2025-07-03 --------------------- Makes the deprecation warning for using the global random instance thread-safe, as part of our work towards thread safety (:issue:`4451`). .. _v6.135.23: --------------------- 6.135.23 - 2025-07-02 --------------------- In order to de-flake ``RecursionError`` failures, Hypothesis sets a deterministic limit on ``sys.setrecursionlimit``. This patch makes the setting of this limit aware of uses by Hypothesis from multiple threads, so it does not produce spurious warnings in multithreaded environments. .. _v6.135.22: --------------------- 6.135.22 - 2025-07-02 --------------------- Improves the thread safety of caching strategy definitions, as well as usage of strategy transformations like |.map| and |.filter|. .. _v6.135.21: --------------------- 6.135.21 - 2025-07-02 --------------------- Fix the thread safety of |@rule| definitions in |RuleBasedStateMachine|. .. _v6.135.20: --------------------- 6.135.20 - 2025-06-30 --------------------- Fixes ``reproduction_decorator`` being missing under :ref:`hypothesis-specific metadata ` in many :ref:`observability ` observations, when it should have been present. .. _v6.135.19: --------------------- 6.135.19 - 2025-06-30 --------------------- Improve threading compatibility of an internal helper for managing deterministic rng seeding. .. _v6.135.18: --------------------- 6.135.18 - 2025-06-30 --------------------- Remove an internal assertion which could trigger if (1) a lambda was present in the source code of a test, (2) and the source code file was edited on disk between the start of the python process and when Hypothesis runs the property. .. _v6.135.17: --------------------- 6.135.17 - 2025-06-30 --------------------- Refactor some internals related to the shrinker for better compatibility with free-threading (:issue:`4451`). .. _v6.135.16: --------------------- 6.135.16 - 2025-06-26 --------------------- Fixes an error when the ``_pytest`` module is present in ``sys.modules``, but *not* the ``_pytest.outcomes`` or ``_pytest.fixtures`` modules. This can happen with code that imports just ``_pytest``, without importing ``pytest``. .. _v6.135.15: --------------------- 6.135.15 - 2025-06-25 --------------------- Temporarily disable the warning when |st.shared| strategies with the same ``key`` draw from different base strategies, due to false alarms. Once we fix the false alarms in a future release, the warning will be re-enabled. .. _v6.135.14: --------------------- 6.135.14 - 2025-06-20 --------------------- Speed up usages of |st.sampled_from| by deferring evaluation of its repr, and truncating its repr for large collections (over 512 elements). This is especially noticeable when using |st.sampled_from| with large collections. The repr of |st.sampled_from| strategies involving sequence classes with custom reprs may change as a result of this release. .. _v6.135.13: --------------------- 6.135.13 - 2025-06-20 --------------------- Fixes a substantial performance regression in stateful tests from computing string representations, present since :ref:`version 6.131.20 `. .. _v6.135.12: --------------------- 6.135.12 - 2025-06-19 --------------------- Fix a rare race condition in internal cache eviction logic. .. _v6.135.11: --------------------- 6.135.11 - 2025-06-17 --------------------- This patch fixes an error when importing :ref:`our django extra ` (via ``hypothesis.extra.django``) if ``django.contrib.auth`` was not in ``INSTALLED_APPS`` (:issue:`3716`). Thanks to Chris Wesseling for this fix! .. _v6.135.10: --------------------- 6.135.10 - 2025-06-15 --------------------- Fix a rare race condition in |ExampleDatabase.fetch|, where we might have read from a non-existent directory. .. _v6.135.9: -------------------- 6.135.9 - 2025-06-13 -------------------- Refactor some internal code related to patches to make it easier to test. .. _v6.135.8: -------------------- 6.135.8 - 2025-06-13 -------------------- Add type hints to internal code for patching. .. _v6.135.7: -------------------- 6.135.7 - 2025-06-12 -------------------- Fixes a race condition in |ExampleDatabase.add_listener| for |DirectoryBasedExampleDatabase| after version :ref:`6.135.1 ` where the listener might have tried to read a file that doesn't exist. .. _v6.135.6: -------------------- 6.135.6 - 2025-06-11 -------------------- This patch corrects the f-string formatting of a few array-related error messages. .. _v6.135.5: -------------------- 6.135.5 - 2025-06-10 -------------------- Improve the error message when applying |@given| to a :pypi:`pytest` fixture with pytest 8.4.0. .. _v6.135.4: -------------------- 6.135.4 - 2025-06-09 -------------------- Further improve the performance of the constants-collection feature introduced in :ref:`version 6.131.1 `, by ignoring large files and files with many constants. .. _v6.135.3: -------------------- 6.135.3 - 2025-06-08 -------------------- This release adds the experimental and unstable |OBSERVABILITY_CHOICES| option for :ref:`observability `. If set, the choice sequence is included in ``metadata.choice_nodes``, and choice sequence spans are included in ``metadata.choice_spans``. These are relatively low-level implementation detail of Hypothesis, and are exposed in observability for users building tools or research on top of Hypothesis. See |PrimitiveProvider| for more details about the choice sequence and choice spans. We are actively working towards a better interface for this. Feel free to use |OBSERVABILITY_CHOICES| to experiment, but don't rely on it yet! .. _v6.135.2: -------------------- 6.135.2 - 2025-06-08 -------------------- This patch restores compatibility when using `the legacy Python 3.9 LL(1) parser `__ yet again, because the fix in :ref:`version 6.131.33 ` was too brittle. Thanks to Marco Ricci for this fix! .. _v6.135.1: -------------------- 6.135.1 - 2025-06-05 -------------------- |DirectoryBasedExampleDatabase| now removes empty directories after |ExampleDatabase.delete| is called. .. _v6.135.0: -------------------- 6.135.0 - 2025-06-03 -------------------- This release adds :func:`~hypothesis.internal.conjecture.provider_conformance.run_conformance_test`, for use in testing implementations of :ref:`alternative backends `. .. _v6.134.0: -------------------- 6.134.0 - 2025-06-03 -------------------- This patch adds :class:`hypothesis.extra.django.SimpleTestCase` (:issue:`4117`) Thanks to Chris Wesseling for this contribution! .. _v6.133.2: -------------------- 6.133.2 - 2025-06-03 -------------------- Internal changes to support `hypofuzz `__. .. _v6.133.1: -------------------- 6.133.1 - 2025-06-03 -------------------- The ``to_json`` hook used internally when writing :ref:`observability ` reports is now supported on nested dataclasses (in addition to outermost dataclasses). .. _v6.133.0: -------------------- 6.133.0 - 2025-06-02 -------------------- Warn when :func:`~hypothesis.strategies.shared` strategies with the same ``key`` draw from different base strategies. This could lead to subtle failures or lower-than-expected example coverage. .. _v6.132.0: -------------------- 6.132.0 - 2025-05-31 -------------------- Add |PrimitiveProvider.on_observation| to the internal :ref:`alternative backends ` interface. .. _v6.131.33: --------------------- 6.131.33 - 2025-05-31 --------------------- This patch restores compatibility when using `the legacy Python 3.9 LL(1) parser `__, which was accidentally broken since :ref:`version 6.130.13 `. Thanks to Marco Ricci for this fix! .. _v6.131.32: --------------------- 6.131.32 - 2025-05-30 --------------------- :ref:`fuzz_one_input ` now writes :ref:`observability reports ` if observability is enabled, bringing it in line with the behavior of other standard ways to invoke a Hypothesis test. .. _v6.131.31: --------------------- 6.131.31 - 2025-05-30 --------------------- Improve documentation of |@example|. .. _v6.131.30: --------------------- 6.131.30 - 2025-05-27 --------------------- This patch resolves a Pandas FutureWarning (:issue:`4400`) caused by indexing with an integer key. .. _v6.131.29: --------------------- 6.131.29 - 2025-05-27 --------------------- The observations passed to |TESTCASE_CALLBACKS| are now dataclasses, rather than dictionaries. The content written to ``.hypothesis/observed`` under ``HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY`` remains the same. .. _v6.131.28: --------------------- 6.131.28 - 2025-05-25 --------------------- Add documentation to some internal APIs. .. _v6.131.27: --------------------- 6.131.27 - 2025-05-24 --------------------- Add ``PrimitiveProvider.replay_choices`` to the :ref:`alternative backends ` interface, to support warm-starting e.g. :pypi:`hypothesis-crosshair` from :pypi:`hypofuzz`. .. _v6.131.26: --------------------- 6.131.26 - 2025-05-24 --------------------- Improve |ExampleDatabase| documentation. .. _v6.131.25: --------------------- 6.131.25 - 2025-05-23 --------------------- Add some internal type hints. .. _v6.131.24: --------------------- 6.131.24 - 2025-05-23 --------------------- Improve |@settings| documentation. .. _v6.131.23: --------------------- 6.131.23 - 2025-05-23 --------------------- This patch adds ``GITLAB_CI`` to the environment variables checked when enabling the default CI |settings| profile. Thanks to Genevieve Mendoza for this contribution! .. _v6.131.22: --------------------- 6.131.22 - 2025-05-22 --------------------- Include |note| and |Phase.explain| output in the "representation" field of :ref:`observability reports ` for failing examples, to more closely match the output produced by Hypothesis. .. _v6.131.21: --------------------- 6.131.21 - 2025-05-21 --------------------- |BackgroundWriteDatabase| instances now defer creating and starting a thread until first use. .. _v6.131.20: --------------------- 6.131.20 - 2025-05-20 --------------------- Improve the string representation of |st.characters| in some cases. .. _v6.131.19: --------------------- 6.131.19 - 2025-05-19 --------------------- Cap the length of bytestrings collected as part of the constants-collection feature introduced in :ref:`version 6.131.1 `, as long bytestrings are unlikely to be useful. .. _v6.131.18: --------------------- 6.131.18 - 2025-05-17 --------------------- All |ExampleDatabase| implementations in Hypothesis now implement ``__eq__``. .. _v6.131.17: --------------------- 6.131.17 - 2025-05-14 --------------------- Further improve the performance of the new features introduced in :ref:`version 6.131.1 `, by improving unions of large sets. .. _v6.131.16: --------------------- 6.131.16 - 2025-05-13 --------------------- Improve performance of an internal method to count possible choices. .. _v6.131.15: --------------------- 6.131.15 - 2025-05-07 --------------------- Improves output when assigning values to multiple target bundles in a stateful rule (:issue:`4361`). .. _v6.131.14: --------------------- 6.131.14 - 2025-05-07 --------------------- Internal optimization to reduce redundant choice sequence spans. .. _v6.131.13: --------------------- 6.131.13 - 2025-05-07 --------------------- Add a ``for_failure: bool = False`` parameter to ``provider.realize`` in :ref:`alternative backends `, so that symbolic-based backends can increase their timeouts when realizing failures, which are more important than regular examples. .. _v6.131.12: --------------------- 6.131.12 - 2025-05-06 --------------------- Improve type hints for the single-argument form of |st.one_of|. ``st.one_of(strategies)`` now matches the type of ``st.one_of(*strategies)``. For instance, ``st.one_of([st.integers(), st.none()])`` now has the correct type of ``SearchStrategy[int | None]`` instead of ``SearchStrategy[Any]``. .. _v6.131.11: --------------------- 6.131.11 - 2025-05-06 --------------------- Fix incorrectly reporting :ref:`alternative backends ` as unsound in some cases. .. _v6.131.10: --------------------- 6.131.10 - 2025-05-06 --------------------- Remove more false-positive locations from |Phase.explain| output, and add a new ``metadata.reproduction_decorator`` field in :ref:`observability reports ` for failing examples. .. _v6.131.9: -------------------- 6.131.9 - 2025-04-25 -------------------- Fix a ``BytesWarning`` after :ref:`version 6.131.1 ` if the source code used the same value in both a normal and binary string. .. _v6.131.8: -------------------- 6.131.8 - 2025-04-23 -------------------- |DirectoryBasedExampleDatabase| will now fall back to (potentially non-atomic) copies rather than renames, if the temporary directory used for atomic write-and-rename is on a different filesystem to the configured database location (:issue:`4335`). .. _v6.131.7: -------------------- 6.131.7 - 2025-04-22 -------------------- Further improve the performance of the new features introduced in :ref:`version 6.131.1 `. .. _v6.131.6: -------------------- 6.131.6 - 2025-04-19 -------------------- This patch makes the new features introduced in :ref:`version 6.131.1 ` much faster, and fixes an internal ``RecursionError`` when working with deeply-nested code. .. _v6.131.5: -------------------- 6.131.5 - 2025-04-18 -------------------- Fix a rare case where database entries were kept after they were no longer needed when using |Phase.target|. .. _v6.131.4: -------------------- 6.131.4 - 2025-04-18 -------------------- Internal refactoring of the |@settings| object, with no user-visible change. .. _v6.131.3: -------------------- 6.131.3 - 2025-04-18 -------------------- Fixes a rare internal error where new code from :ref:`version 6.131.1 ` could fail if :py:data:`sys.modules` is simultaneously modified, e.g. as a side effect of imports executed from another thread. Our :ref:`thread-safety-policy` does not promise that this is supported, but we're happy to take reasonable fixes. Thanks to Tony Li for reporting and fixing this issue. .. _v6.131.2: -------------------- 6.131.2 - 2025-04-17 -------------------- The pub-sub change listening interface of the :ref:`Hypothesis database ` now correctly fires events for |DirectoryBasedExampleDatabase| if the directory was created after the listener was added. Also disables on emscripten the constants-extraction feature introduced in :ref:`v6.131.1`, where it caused substantial slowdown. .. _v6.131.1: -------------------- 6.131.1 - 2025-04-17 -------------------- Hypothesis now looks for constant values in the source code of your program, and sometimes uses them while generating examples. This lets Hypothesis generate interesting inputs that are specific to your program. .. _v6.131.0: -------------------- 6.131.0 - 2025-04-10 -------------------- Add |is_hypothesis_test|, for third-party libraries which want to determine whether a test has been defined with Hypothesis. .. _v6.130.13: --------------------- 6.130.13 - 2025-04-09 --------------------- Refactor some internals. .. _v6.130.12: --------------------- 6.130.12 - 2025-04-09 --------------------- Lays some groundwork for future work on collecting interesting literals from the code being tested, for increased bug-finding power (:issue:`3127`). There is no user-visible change (yet!) .. _v6.130.11: --------------------- 6.130.11 - 2025-04-08 --------------------- Fix the caching behavior of |st.sampled_from|, which in rare cases led to failing an internal assertion (:issue:`4339`). .. _v6.130.10: --------------------- 6.130.10 - 2025-04-07 --------------------- This patch deprecates creating a database using the abstract ``ExampleDatabase()`` class. Use one of the following instead: * Replace ``ExampleDatabase(":memory:")`` with |InMemoryExampleDatabase|. * Replace ``ExampleDatabase("/path/to/dir")`` with |DirectoryBasedExampleDatabase|. * Replace ``ExampleDatabase()`` with either |InMemoryExampleDatabase| or |DirectoryBasedExampleDatabase|, depending on your needs. Previously, Hypothesis interpreted ``ExampleDatabase()`` as a |DirectoryBasedExampleDatabase| in the default ``.hypothesis`` directory, with a fallback to |InMemoryExampleDatabase| if that location was not available. .. _v6.130.9: -------------------- 6.130.9 - 2025-04-06 -------------------- When reporting the always-failing, never-passing lines from the |Phase.explain| phase, we now sort the reported lines so that local code shows up first, then third-party library code, then standard library code. .. _v6.130.8: -------------------- 6.130.8 - 2025-04-02 -------------------- Improves the documentation of |settings.Verbosity| objects. .. _v6.130.7: -------------------- 6.130.7 - 2025-04-02 -------------------- Rename internal variables for clarity. .. _v6.130.6: -------------------- 6.130.6 - 2025-03-31 -------------------- Update the documentation link in |HealthCheck| error messages to their new location in the documentation. .. _v6.130.5: -------------------- 6.130.5 - 2025-03-28 -------------------- Improve our internal type hints. .. _v6.130.4: -------------------- 6.130.4 - 2025-03-25 -------------------- Improve an additional interaction between the :pypi:`hypothesis-crosshair` :ref:`backend ` and :ref:`our observability tools `. .. _v6.130.3: -------------------- 6.130.3 - 2025-03-24 -------------------- This patch improves the interaction between the :pypi:`hypothesis-crosshair` :ref:`backend ` and :ref:`our observability tools `. .. _v6.130.2: -------------------- 6.130.2 - 2025-03-22 -------------------- Fix an issue with realizing symbolic values provided by :ref:`alternative backends ` when Hypothesis encounters an internal error in its engine. .. _v6.130.1: -------------------- 6.130.1 - 2025-03-22 -------------------- Improve the documentation for some strategies, including |st.composite| and |st.data|. .. _v6.130.0: -------------------- 6.130.0 - 2025-03-21 -------------------- Nesting :func:`@given ` inside of :func:`@given ` is now a |HealthCheck| failure. Nesting :func:`@given ` results in quadratic generation and shrinking behavior, and can usually be more cleanly expressed by replacing the inner function with a :func:`~hypothesis.strategies.data` parameter on the outer given. For more details, see :obj:`~hypothesis.HealthCheck.nested_given`. (:issue:`4167`) .. _v6.129.5: -------------------- 6.129.5 - 2025-03-21 -------------------- Fixes an internal error when certain :ref:`alternative backends ` find a failure on their very first generated example. .. _v6.129.4: -------------------- 6.129.4 - 2025-03-18 -------------------- :func:`~hypothesis.strategies.nothing` is now typed as ``SearchStrategy[Never]``, because no value can ever be drawn from it. This may help type checkers statically determine that some code is not reachable. .. _v6.129.3: -------------------- 6.129.3 - 2025-03-16 -------------------- This patch improves the string representation of :func:`~hypothesis.strategies.fixed_dictionaries`. .. _v6.129.2: -------------------- 6.129.2 - 2025-03-14 -------------------- Improve how the shrinker checks for unnecessary work, leading to 10% less time spent shrinking on average, with no reduction in quality. .. _v6.129.1: -------------------- 6.129.1 - 2025-03-13 -------------------- :func:`~hypothesis.strategies.randoms` no longer produces ``1.0``, matching the exclusive upper bound of :obj:`random.Random.random` (:issue:`4297`). .. _v6.129.0: -------------------- 6.129.0 - 2025-03-11 -------------------- This release adds a ``"hypothesis-urandom"`` :ref:`backend `, which draws randomness from ``/dev/urandom`` instead of Python's PRNG. This is useful for users of `Antithesis `_ who also have Hypothesis tests, allowing Antithesis mutation of ``/dev/urandom`` to drive Hypothesis generation. We expect it to be strictly slower than the default backend for everyone else. It can be enabled with ``@settings(backend="hypothesis-urandom")``. .. _v6.128.3: -------------------- 6.128.3 - 2025-03-11 -------------------- For strategies which draw make recursive draws, including :func:`~hypothesis.strategies.recursive` and :func:`~hypothesis.strategies.deferred`, we now generate examples with duplicated subtrees more often. This tends to uncover interesting behavior in tests. For instance, we might now generate a tree like this more often (though the details depend on the strategy): .. code-block:: none ┌─────┐ ┌──────┤ a ├──────┐ │ └─────┘ │ ┌──┴──┐ ┌──┴──┐ │ b │ │ a │ └──┬──┘ └──┬──┘ ┌────┴────┐ ┌────┴────┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ │ c │ │ d │ │ b │ │ ... │ └─────┘ └─────┘ └──┬──┘ └─────┘ ┌────┴────┐ ┌──┴──┐ ┌──┴──┐ │ c │ │ d │ └─────┘ └─────┘ .. _v6.128.2: -------------------- 6.128.2 - 2025-03-10 -------------------- Improves input validation for several strategies in our :ref:`pandas extra `, so that they raise a helpful ``InvalidArgument`` rather than ``OverflowError``. Discovered by our recent :ref:`string generation upgrade `. .. _v6.128.1: -------------------- 6.128.1 - 2025-03-09 -------------------- Rename a few internal classes for clarity. .. _v6.128.0: -------------------- 6.128.0 - 2025-03-09 -------------------- :func:`~hypothesis.strategies.text` now occasionally generates from a preselected list of strings which are likely to find bugs. These include ligatures, right-to-left and top-to-bottom text, emojis, emoji modifiers, strings like ``"Infinity"``, ``"None"``, and ``"FALSE"``, and other interesting things. This is especially useful when testing the full unicode range, where the search space is too large for uniform sampling to be very effective. Of course, examples generated this way shrink just like they normally would. It was always possible for Hypothesis to generate these strings; it is just more likely after this change. From the outside, it is as if Hypothesis generated the example completely randomly. Many thanks to the `Big List of Naughty Strings `_, `Text Rendering Hates You `_, and `Text Editing Hates You Too `_ for forming the basis of this list. .. _v6.127.9: -------------------- 6.127.9 - 2025-03-06 -------------------- We now provide a better string representation for :func:`~hypothesis.strategies.one_of` strategies, by flattening consecutive ``|`` combinations. For instance: .. code-block:: pycon >>> st.integers() | st.text() | st.booleans() # previously: one_of(one_of(integers(), text()), booleans()) one_of(integers(), text(), booleans()) Explicit calls to :func:`~hypothesis.strategies.one_of` remain unflattened, in order to make tracking down complicated :func:`~hypothesis.strategies.one_of` constructions easier: .. code-block:: pycon >>> st.one_of(st.integers(), st.one_of(st.text(), st.booleans())) one_of(integers(), one_of(text(), booleans())) We print ``one_of`` in reprs (rather than ``integers() | text() | ...``) for consistency with reprs containing ``.filter`` or ``.map`` calls, which uses the full ``one_of`` to avoid ambiguity. .. _v6.127.8: -------------------- 6.127.8 - 2025-03-06 -------------------- This patch improves shrinking behavior for values from :func:`~hypothesis.strategies.text` and :func:`~hypothesis.strategies.binary` which contain duplicate elements, like ``"zzzabc"``. It also improves shrinking for bugs which require the same character to be drawn from two different :func:`~hypothesis.strategies.text` strategies to trigger. .. _v6.127.7: -------------------- 6.127.7 - 2025-03-05 -------------------- Fix a type-hinting regression from :ref:`version 6.125.1 `, where we would no longer guarantee the type of the argument to ``.filter`` predicates (:issue:`4269`). .. code-block:: python # x was previously Unknown, but is now correctly guaranteed to be int st.integers().filter(lambda x: x > 0) .. _v6.127.6: -------------------- 6.127.6 - 2025-03-04 -------------------- This patch tweaks the performance of |Phase.target|, avoiding aborting some test cases when it would be better to finish generating them. .. _v6.127.5: -------------------- 6.127.5 - 2025-03-03 -------------------- This patch fixes a bug where :func:`~hypothesis.strategies.from_type` would error on certain types involving :class:`~python:typing.Protocol` (:issue:`4194`). .. _v6.127.4: -------------------- 6.127.4 - 2025-03-02 -------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.127.3: -------------------- 6.127.3 - 2025-02-28 -------------------- Improve shrinking of non-standard NaN float values (:issue:`4277`). .. _v6.127.2: -------------------- 6.127.2 - 2025-02-24 -------------------- Adjust type hints for the pub-sub database implementation in :ref:`version 6.126.0 `, and remove a remnant debug print in its implementation. .. _v6.127.1: -------------------- 6.127.1 - 2025-02-23 -------------------- Improve the clarity of printing counterexamples in :ref:`stateful testing `, by avoiding confusing :class:`~hypothesis.stateful.Bundle` references with equivalent values drawn from a regular strategy. For example, we now print: .. code-block:: python a_0 = state.add_to_bundle(a=0) state.unrelated(value=0) instead of .. code-block:: python a_0 = state.add_to_bundle(a=0) state.unrelated(value=a_0) if the ``unrelated`` rule draws from a regular strategy such as :func:`~hypothesis.strategies.integers` instead of the ``a`` bundle. .. _v6.127.0: -------------------- 6.127.0 - 2025-02-23 -------------------- This releases adds support for type aliases created with the :py:keyword:`type` statement (new in python 3.12) to :func:`~hypothesis.strategies.from_type` and :func:`~hypothesis.strategies.register_type_strategy`. .. _v6.126.0: -------------------- 6.126.0 - 2025-02-18 -------------------- The :ref:`Hypothesis database ` now supports a pub-sub interface to efficiently listen for changes in the database, via ``.add_listener`` and ``.remove_listener``. While all databases that ship with Hypothesis support this interface, implementing it is not required for custom database subclasses. Hypothesis will warn when trying to listen on a database without support. This feature is currently only used downstream in `hypofuzz `_. .. _v6.125.3: -------------------- 6.125.3 - 2025-02-11 -------------------- Improves sharing of some internal cache behavior. .. _v6.125.2: -------------------- 6.125.2 - 2025-02-06 -------------------- Optimize performance (improves speed by ~5%) and clarify the wording in an error message. .. _v6.125.1: -------------------- 6.125.1 - 2025-02-03 -------------------- Fixes a bug since around :ref:`version 6.124.4 ` where we might have generated ``-0.0`` for ``st.floats(min_value=0.0)``, which is unsound. .. _v6.125.0: -------------------- 6.125.0 - 2025-02-03 -------------------- Add 2024.12 to the list of recognized Array API versions in ``hypothesis.extra.array_api``. .. _v6.124.9: -------------------- 6.124.9 - 2025-02-01 -------------------- Registration of experimental :ref:`alternative-backends` is now done via ``hypothesis.internal.conjecture.providers.AVAILABLE_PROVIDERS`` instead of ``hypothesis.internal.conjecture.data.AVAILABLE_PROVIDERS``. .. _v6.124.8: -------------------- 6.124.8 - 2025-02-01 -------------------- Refactor some internals for better type hinting. .. _v6.124.7: -------------------- 6.124.7 - 2025-01-25 -------------------- Internal renamings. .. _v6.124.6: -------------------- 6.124.6 - 2025-01-25 -------------------- More work on internal type hints. .. _v6.124.5: -------------------- 6.124.5 - 2025-01-25 -------------------- Internal refactoring to make some stateful internals easier to access. .. _v6.124.4: -------------------- 6.124.4 - 2025-01-25 -------------------- Refactoring of our internal input generation. This shouldn't lead to any changes in the distribution of test inputs. If you notice any, please open an issue! .. _v6.124.3: -------------------- 6.124.3 - 2025-01-24 -------------------- Some Hypothesis internals now use the number of choices as a yardstick of input size, rather than the entropy consumed by those choices. We don't expect this to cause significant behavioral changes. .. _v6.124.2: -------------------- 6.124.2 - 2025-01-21 -------------------- Improves our internal caching logic for test cases. .. _v6.124.1: -------------------- 6.124.1 - 2025-01-18 -------------------- :ref:`fuzz_one_input ` is now implemented using an :ref:`alternative backend `. This brings the interpretation of the fuzzer-provided bytestring closer to the fuzzer mutations, allowing the mutations to work more reliably. We hope to use this backend functionality to improve fuzzing integration (e.g. `atheris issue #20 `__) in the future! .. _v6.124.0: -------------------- 6.124.0 - 2025-01-16 -------------------- The :ref:`Hypothesis example database ` now uses a new internal format to store examples. This new format is not compatible with the previous format, so stored entries will not carry over. The database is best thought of as a cache that may be invalidated at times. Instead of relying on it for correctness, we recommend using :obj:`@example ` to specify explicit examples. When using databases across environments (such as connecting a :class:`~hypothesis.database.GitHubArtifactDatabase` database in CI to your local environment), we recommend using the same version of Hypothesis for each where possible, for maximum reproducibility. .. _v6.123.17: --------------------- 6.123.17 - 2025-01-13 --------------------- This patch improves certain corner cases for reporting of flaky errors (:issue:`4183` and :issue:`4228`). .. _v6.123.16: --------------------- 6.123.16 - 2025-01-13 --------------------- Improves an edge case in one of our integer and float shrinking passes. .. _v6.123.15: --------------------- 6.123.15 - 2025-01-11 --------------------- Improves one of our shrinking passes for integers which require a constant relative difference to trigger the bug. .. _v6.123.14: --------------------- 6.123.14 - 2025-01-11 --------------------- Avoid realizing symbolic values from :ref:`alternative-backends` when |Verbosity| is ``verbose`` or higher. .. _v6.123.13: --------------------- 6.123.13 - 2025-01-09 --------------------- More internal code refactoring. .. _v6.123.12: --------------------- 6.123.12 - 2025-01-09 --------------------- :class:`~hypothesis.database.DirectoryBasedExampleDatabase` now creates files representing database entries atomically, avoiding a very brief intermediary state where a file could be created but not yet written to. .. _v6.123.11: --------------------- 6.123.11 - 2025-01-09 --------------------- Internal code refactoring. .. _v6.123.10: --------------------- 6.123.10 - 2025-01-09 --------------------- Fixes a bug caused by :ref:`alternative backends ` raising ``hypothesis.errors.BackendCannotProceed`` in certain cases. .. _v6.123.9: -------------------- 6.123.9 - 2025-01-08 -------------------- Add internal type hints to our pretty printer. .. _v6.123.8: -------------------- 6.123.8 - 2025-01-08 -------------------- The shrinker contains a pass aimed at integers which are required to sum to a value. This patch extends that pass to floats as well. .. _v6.123.7: -------------------- 6.123.7 - 2025-01-07 -------------------- Internal type hint additions and refactorings. .. _v6.123.6: -------------------- 6.123.6 - 2025-01-07 -------------------- :func:`@reproduce_failure() ` now uses a newer internal interface to represent failures. As a reminder, this representation is not intended to be stable across versions or with respect to changes in the test. .. _v6.123.5: -------------------- 6.123.5 - 2025-01-07 -------------------- Internal code refactoring for the typed choice sequence (:issue:`3921`). May have some neutral effect on shrinking. .. _v6.123.4: -------------------- 6.123.4 - 2025-01-06 -------------------- This patch improves shrinking involving long strings or byte sequences whose value is not relevant to the failure. .. _v6.123.3: -------------------- 6.123.3 - 2025-01-06 -------------------- This release further improves shrinking of strategies using :func:`~hypothesis.strategies.one_of`, allowing the shrinker to more reliably move between branches of the strategy. .. _v6.123.2: -------------------- 6.123.2 - 2024-12-27 -------------------- The shrinker now uses the typed choice sequence (:issue:`3921`) when ordering failing examples. As a result, Hypothesis may now report a different minimal failing example for some tests. We expect most cases to remain unchanged. .. _v6.123.1: -------------------- 6.123.1 - 2024-12-24 -------------------- Our pytest plugin now emits a warning if you set Pytest's ``norecursedirs`` config option in such a way that the ``.hypothesis`` directory would be searched for tests. This reliably indicates that you've made a mistake which slows down test collection, usually assuming that your configuration extends the set of ignored patterns when it actually replaces them. (:issue:`4200`) .. _v6.123.0: -------------------- 6.123.0 - 2024-12-23 -------------------- :func:`~hypothesis.strategies.from_type` can now handle constructors with required positional-only arguments if they have type annotations. Previously, we only passed arguments by keyword. .. _v6.122.7: -------------------- 6.122.7 - 2024-12-23 -------------------- This patch lays some groundwork for migrating our internal representation to the typed choice sequence (:issue:`3921`) .. _v6.122.6: -------------------- 6.122.6 - 2024-12-21 -------------------- This patch cleans up some internal code around clamping floats. .. _v6.122.5: -------------------- 6.122.5 - 2024-12-20 -------------------- This release improves shrinking in some cases, especially for strategies using :func:`~hypothesis.strategies.one_of`. This will typically improve shrinking speed and may in some cases improve the end result. .. _v6.122.4: -------------------- 6.122.4 - 2024-12-19 -------------------- This patch improves generation performance for the provisional :func:`~hypothesis.provisional.domains` strategy, including its derivative strategies :func:`~hypothesis.provisional.urls` and :func:`~hypothesis.strategies.emails`. .. _v6.122.3: -------------------- 6.122.3 - 2024-12-08 -------------------- This patch improves our error and warning messages. - Add a warning for ``st.text("ascii")`` - you probably meant ``st.text(st.characters(codec="ascii"))``. Similarly for ``"utf-8"``. - Recommend remedies in the error message of ``Unsatisfiable``. - When ``@given`` errors because it was given an extra keyword argument, and the keyword matches a setting name like ``max_examples``, recommend ``@settings(max_examples=...)`` instead. .. _v6.122.2: -------------------- 6.122.2 - 2024-12-08 -------------------- This patch updates some outdated external links in our documentation. .. _v6.122.1: -------------------- 6.122.1 - 2024-12-01 -------------------- Fix :func:`~hypothesis.strategies.from_type` on :class:`collections.abc.Callable` returning ``None``. .. _v6.122.0: -------------------- 6.122.0 - 2024-11-29 -------------------- This release adds ``.span_start()`` and ``.span_end()`` methods to our internal ``PrimitiveProvider`` interface, for use by :ref:`alternative-backends`. .. _v6.121.2: -------------------- 6.121.2 - 2024-11-29 -------------------- This patch updates our autoformatting tools, improving our code style without any API changes. .. _v6.121.1: -------------------- 6.121.1 - 2024-11-29 -------------------- This release brings back the old representation of :class:`hypothesis.stateful.Bundle`, reverting most changes of `PR #4124 `_. .. _v6.121.0: -------------------- 6.121.0 - 2024-11-28 -------------------- This release adds :class:`~hypothesis.database.BackgroundWriteDatabase`, a new database backend which defers writes on the wrapped database to a background thread. This allows for low-overhead writes in performance-critical environments like :ref:`fuzz_one_input `. .. _v6.120.0: -------------------- 6.120.0 - 2024-11-27 -------------------- * This release changes our input distribution for low ``max_examples``. Previously, we capped the size of inputs when generating at least the first 10 inputs, with the reasoning that early inputs to a property should be small. However, this meant properties with ``max_examples=10`` would consistent entirely of small inputs. This patch removes the hard lower bound so that inputs to these properties are more representative of the input space. * When a user requests an interactive input via ``strategy.example``, we generate and cache a batch of 100 inputs, returning the first one. This can be expensive for large strategies or when only a few examples are needed. This release improves the speed of ``strategy.example`` by lowering the batch size to 10. .. _v6.119.4: -------------------- 6.119.4 - 2024-11-22 -------------------- This patch fixes a bug since :ref:`v6.99.13` where only interactively-generated values (via ``data.draw``) would be reported in the ``arguments`` field of our :ref:`observability output `. Now, all values are reported. .. _v6.119.3: -------------------- 6.119.3 - 2024-11-17 -------------------- Hypothesis collects coverage information during the |Phase.shrink| and |Phase.explain| phases in order to show a more informative error message. On 3.12+, this uses :mod:`sys.monitoring`. This patch improves the performance of coverage collection on 3.12+ by disabling events we don't need. .. _v6.119.2: -------------------- 6.119.2 - 2024-11-17 -------------------- This patch refactors some internals to prepare for future work using our IR (:issue:`3921`). .. _v6.119.1: -------------------- 6.119.1 - 2024-11-15 -------------------- This patch migrates some more internals (around generating novel inputs) to the IR layer (:issue:`3921`). .. _v6.119.0: -------------------- 6.119.0 - 2024-11-15 -------------------- This release improves Hypothesis' handling of ExceptionGroup - it's now able to detect marker detections if they're inside a group and attempts to resolve them. Note that this handling is still a work in progress and might not handle edge cases optimally. Please open issues if you encounter any problems or unexpected behavior with it. .. _v6.118.9: -------------------- 6.118.9 - 2024-11-15 -------------------- Internal refactorings in preparation for upcoming changes. .. _v6.118.8: -------------------- 6.118.8 - 2024-11-12 -------------------- Internal renamings. .. _v6.118.7: -------------------- 6.118.7 - 2024-11-10 -------------------- This patch removes some ``# type: ignore`` comments following a :pypi:`mypy` update. .. _v6.118.6: -------------------- 6.118.6 - 2024-11-10 -------------------- When Hypothesis replays examples from its test database that it knows were previously fully shrunk it will no longer try to shrink them again. This should significantly speed up development workflows for slow tests, as the shrinking could contribute a significant delay when rerunning the tests. In some rare cases this may cause minor reductions in example quality. This was considered an acceptable tradeoff for the improved test runtime. .. _v6.118.5: -------------------- 6.118.5 - 2024-11-10 -------------------- This patch avoids computing some string representations we won't need, giving a small speedup (part of :issue:`4139`). .. _v6.118.4: -------------------- 6.118.4 - 2024-11-10 -------------------- This patch migrates the optimisation algorithm for :ref:`targeted property-based testing ` to our IR layer (:issue:`3921`). This should result in moderately different (and hopefully improved) exploration behavior in tests which use :func:`hypothesis.target`. .. _v6.118.3: -------------------- 6.118.3 - 2024-11-10 -------------------- This patch adds more type hints to internal Hypothesis code. .. _v6.118.2: -------------------- 6.118.2 - 2024-11-09 -------------------- This patch migrates the |Phase.explain| phase to our IR layer (:issue:`3921`). This should improve both its speed and precision. .. _v6.118.1: -------------------- 6.118.1 - 2024-11-09 -------------------- This patch updates some internals around how we determine an input is too large to finish generating. .. _v6.118.0: -------------------- 6.118.0 - 2024-11-08 -------------------- The :func:`~hypothesis.provisional.urls` strategy no longer generates URLs where the port number is 0. This change is motivated by the idea that the generated URLs should, at least in theory, be possible to fetch. The port number 0 is special; if a server binds to port 0, the kernel will allocate an unused, and non-zero, port instead. That means that it's not possible for a server to actually be listening on port 0. This motivation is briefly described in the documentation for :func:`~hypothesis.provisional.urls`. Thanks to @gmacon for fixing :issue:`4157`! .. _v6.117.0: -------------------- 6.117.0 - 2024-11-07 -------------------- This changes the behaviour of settings profiles so that if you reregister the currently loaded profile it will automatically reload it. Previously you would have had to load it again. In particular this means that if you register a "ci" profile, it will automatically be used when Hypothesis detects you are running on CI. .. _v6.116.0: -------------------- 6.116.0 - 2024-11-01 -------------------- Hypothesis now detects if it is running on a CI server and provides better default settings for running on CI in this case. .. _v6.115.6: -------------------- 6.115.6 - 2024-10-30 -------------------- This patch changes the priority order of pretty printing logic so that a user provided pretty printing method will always be used in preference to e.g. printing it like a dataclass. .. _v6.115.5: -------------------- 6.115.5 - 2024-10-23 -------------------- This patch restores diversity to the outputs of :func:`from_type(type) ` (:issue:`4144`). .. _v6.115.4: -------------------- 6.115.4 - 2024-10-23 -------------------- This release improves pretty printing of nested classes to include the outer class name in their printed representation. .. _v6.115.3: -------------------- 6.115.3 - 2024-10-16 -------------------- This patch fixes a regression from :ref:`version 6.115.2 ` where generating values from :func:`~hypothesis.strategies.integers` with certain values for ``min_value`` and ``max_value`` would error. .. _v6.115.2: -------------------- 6.115.2 - 2024-10-14 -------------------- This release improves integer shrinking by folding the endpoint upweighting for :func:`~hypothesis.strategies.integers` into the ``weights`` parameter of our IR (:issue:`3921`). If you maintain an alternative backend as part of our (for now explicitly unstable) :ref:`alternative-backends`, this release changes the type of the ``weights`` parameter to ``draw_integer`` and may be a breaking change for you. .. _v6.115.1: -------------------- 6.115.1 - 2024-10-14 -------------------- This patch improves the performance of :func:`~hypothesis.strategies.from_type` with `pydantic.types.condate `__ (:issue:`4000`). .. _v6.115.0: -------------------- 6.115.0 - 2024-10-12 -------------------- This improves the formatting of dataclasses and attrs classes when printing falsifying examples. .. _v6.114.1: -------------------- 6.114.1 - 2024-10-10 -------------------- This patch upgrades remaining type annotations to Python 3.9 syntax. .. _v6.114.0: -------------------- 6.114.0 - 2024-10-09 -------------------- This release drops support for Python 3.8, `which reached end of life on 2024-10-07 `__. .. _v6.113.0: -------------------- 6.113.0 - 2024-10-09 -------------------- This release adds ``hypothesis.errors.BackendCannotProceed``, an unstable API for use by :ref:`alternative-backends`. .. _v6.112.5: -------------------- 6.112.5 - 2024-10-08 -------------------- This release fixes a regression where :class:`hypothesis.stateful.Bundle` did not work properly with |.flatmap| functionality (:issue:`4128`). .. _v6.112.4: -------------------- 6.112.4 - 2024-10-06 -------------------- This patch tweaks the paths in ``@example(...)`` patches, so that both ``git apply`` and ``patch`` will work by default. .. _v6.112.3: -------------------- 6.112.3 - 2024-10-05 -------------------- This release refactors internals of :class:`hypothesis.stateful.Bundle` to have a more consistent representation internally. .. _v6.112.2: -------------------- 6.112.2 - 2024-09-29 -------------------- This patch fixes an internal error when the ``__context__`` attribute of a raised exception leads to a cycle (:issue:`4115`). .. _v6.112.1: -------------------- 6.112.1 - 2024-09-13 -------------------- This patch removes a now-incorrect internal assertion about numpy's typing after recent numpy changes (currently only in numpy's nightly release). .. _v6.112.0: -------------------- 6.112.0 - 2024-09-05 -------------------- This release adds support for variable-width bytes in our IR layer (:issue:`3921`), which should mean improved performance anywhere you use :func:`~hypothesis.strategies.binary`. If you maintain an alternative backend as part of our (for now explicitly unstable) :ref:`alternative-backends`, this release changes the ``draw_*`` interface and may be a breaking change for you. .. _v6.111.2: -------------------- 6.111.2 - 2024-08-24 -------------------- This patch contains some internal code cleanup. There is no user-visible change. .. _v6.111.1: -------------------- 6.111.1 - 2024-08-15 -------------------- This patch improves shrinking in cases involving 'slips' from one strategy to another. Highly composite strategies are the most likely to benefit from this change. This patch also reduces the range of :class:`python:datetime.datetime` generated by :func:`~hypothesis.extra.django.from_model` in order to avoid https://code.djangoproject.com/ticket/35683. .. _v6.111.0: -------------------- 6.111.0 - 2024-08-11 -------------------- :ref:`alternative-backends` can now implement ``.observe_test_case()`` and ``observe_information_message()`` methods, to record backend-specific metadata and messages in our :ref:`observability output ` (:issue:`3845` and `hypothesis-crosshair#22 `__). .. _v6.110.2: -------------------- 6.110.2 - 2024-08-11 -------------------- Support ``__default__`` field of :obj:`~python:typing.TypeVar` and support the same from :pypi:`typing-extensions` in :func:`~hypothesis.strategies.from_type`. .. _v6.110.1: -------------------- 6.110.1 - 2024-08-08 -------------------- Add better error message for :obj:`~python:typing.TypeIs` types in :func:`~hypothesis.strategies.from_type`. .. _v6.110.0: -------------------- 6.110.0 - 2024-08-07 -------------------- Support :obj:`~python:typing.LiteralString` in :func:`~hypothesis.strategies.from_type`. .. _v6.109.1: -------------------- 6.109.1 - 2024-08-07 -------------------- This patch makes progress towards adding type hints to our internal conjecture engine (:issue:`3074`). .. _v6.109.0: -------------------- 6.109.0 - 2024-08-07 -------------------- This release allows using :obj:`~python:typing.Annotated` and :obj:`!ReadOnly` types for :obj:`~python:typing.TypedDict` value types with :func:`~hypothesis.strategies.from_type`. .. _v6.108.10: --------------------- 6.108.10 - 2024-08-06 --------------------- This patch fixes compatibility with :pypi:`attrs==24.1.0 ` on the nightly build of CPython, 3.14.0 pre-alpha (:issue:`4067`). .. _v6.108.9: -------------------- 6.108.9 - 2024-08-05 -------------------- This patch removes an assertion which was in fact possible in rare circumstances involving a small number of very large draws. .. _v6.108.8: -------------------- 6.108.8 - 2024-08-04 -------------------- This patch improves our example generation performance by adjusting our internal cache implementation. .. _v6.108.7: -------------------- 6.108.7 - 2024-08-04 -------------------- This patch improves our pretty-printer for unusual numbers. - Signalling NaNs are now represented by using the :mod:`struct` module to show the exact value by converting from a hexadecimal integer - CPython `limits integer-to-string conversions `__ to mitigate DDOS attacks. We now use hexadecimal for very large integers, and include underscore separators for integers with ten or more digits. .. _v6.108.6: -------------------- 6.108.6 - 2024-08-04 -------------------- This patch improves generation speed in some cases by avoiding pretty-printing overhead for non-failing examples. .. _v6.108.5: -------------------- 6.108.5 - 2024-07-28 -------------------- This patch fixes a rare internal error when using :func:`~hypothesis.strategies.integers` with a high number of examples and certain ``{min, max}_value`` parameters (:pull:`4059`). .. _v6.108.4: -------------------- 6.108.4 - 2024-07-22 -------------------- This patch addresses the issue of hypothesis potentially accessing mocked ``time.perf_counter`` during test execution (:issue:`4051`). .. _v6.108.3: -------------------- 6.108.3 - 2024-07-22 -------------------- Minor internal-only cleanups to some error-handling and reporting code. .. _v6.108.2: -------------------- 6.108.2 - 2024-07-15 -------------------- This patch disables :func:`hypothesis.target` on alternative backends where it would not work. .. _v6.108.1: -------------------- 6.108.1 - 2024-07-14 -------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.108.0: -------------------- 6.108.0 - 2024-07-13 -------------------- This patch changes most ``Flaky`` errors to use an :class:`ExceptionGroup`, which makes the representation of these errors easier to understand. .. _v6.107.0: -------------------- 6.107.0 - 2024-07-13 -------------------- The ``alphabet=`` argument to :func:`~hypothesis.strategies.from_regex` now accepts unions of :func:`~hypothesis.strategies.characters` and :func:`~hypothesis.strategies.sampled_from` strategies, in addition to accepting each individually. This patch also fixes a bug where ``text(...).filter(re.compile(...).match)`` could generate non-matching instances if the regex pattern contained ``|`` (:issue:`4008`). .. _v6.106.1: -------------------- 6.106.1 - 2024-07-12 -------------------- This patch improves our pretty-printer (:issue:`4037`). It also fixes the codemod for ``HealthCheck.all()`` from :ref:`version 6.72 `, which was instead trying to fix ``Healthcheck.all()`` - note the lower-case ``c``! Since our tests had the same typo, it all looked good... until :issue:`4030`. .. _v6.106.0: -------------------- 6.106.0 - 2024-07-12 -------------------- This release improves support for unions of :pypi:`numpy` dtypes such as ``np.float64 | np.complex128`` in :func:`~hypothesis.strategies.from_type` and :func:`~hypothesis.extra.numpy.arrays` (:issue:`4041`). .. _v6.105.2: -------------------- 6.105.2 - 2024-07-12 -------------------- This patch improves the reporting of certain flaky errors. .. _v6.105.1: -------------------- 6.105.1 - 2024-07-07 -------------------- This patch iterates on our experimental support for alternative backends (:ref:`alternative-backends`). See :pull:`4029` for details. .. _v6.105.0: -------------------- 6.105.0 - 2024-07-04 -------------------- This release improves support for Django 5.0, and drops support for end-of-life Django versions (< 4.2). Thanks to Joshua Munn for this contribution. .. _v6.104.4: -------------------- 6.104.4 - 2024-07-04 -------------------- Clean up internal cache implementation. .. _v6.104.3: -------------------- 6.104.3 - 2024-07-04 -------------------- This patch updates our autoformatting tools, improving our code style without any API changes. .. _v6.104.2: -------------------- 6.104.2 - 2024-06-29 -------------------- This patch fixes an issue when realizing symbolics with our experimental :obj:`~hypothesis.settings.backend` setting. .. _v6.104.1: -------------------- 6.104.1 - 2024-06-25 -------------------- Improves internal test coverage. .. _v6.104.0: -------------------- 6.104.0 - 2024-06-24 -------------------- This release adds strategies for Django's ``ModelChoiceField`` and ``ModelMultipleChoiceField`` (:issue:`4010`). Thanks to Joshua Munn for this contribution. .. _v6.103.5: -------------------- 6.103.5 - 2024-06-24 -------------------- Fixes and reinstates full coverage of internal tests, which was accidentally disabled in :pull:`3935` (:issue:`4003`). .. _v6.103.4: -------------------- 6.103.4 - 2024-06-24 -------------------- This release prevents a race condition inside internal cache implementation. .. _v6.103.3: -------------------- 6.103.3 - 2024-06-24 -------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.103.2: -------------------- 6.103.2 - 2024-06-14 -------------------- This patch improves our deduplication tracking across all strategies (:pull:`4007`). Hypothesis is now less likely to generate the same input twice. .. _v6.103.1: -------------------- 6.103.1 - 2024-06-05 -------------------- Account for time spent in garbage collection during tests, to avoid flaky ``DeadlineExceeded`` errors as seen in :issue:`3975`. Also fixes overcounting of stateful run times, a minor observability bug dating to :ref:`version 6.98.9 ` (:pull:`3890`). .. _v6.103.0: -------------------- 6.103.0 - 2024-05-29 -------------------- This release migrates the shrinker to our new internal representation, called the IR layer (:pull:`3962`). This improves the shrinker's performance in the majority of cases. For example, on the Hypothesis test suite, shrinking is a median of 1.38x faster. It is possible this release regresses performance while shrinking certain strategies. If you encounter strategies which reliably shrink more slowly than they used to (or shrink slowly at all), please open an issue! You can read more about the IR layer at :issue:`3921`. .. _v6.102.6: -------------------- 6.102.6 - 2024-05-23 -------------------- This patch fixes one of our shrinking passes getting into a rare ``O(n)`` case instead of ``O(log(n))``. .. _v6.102.5: -------------------- 6.102.5 - 2024-05-22 -------------------- This patch fixes some introspection errors new in Python 3.11.9 and 3.13.0b1, for the Ghostwriter and :func:`~hypothesis.strategies.from_type`. .. _v6.102.4: -------------------- 6.102.4 - 2024-05-15 -------------------- Internal developer documentation, no user-visible changes. .. _v6.102.3: -------------------- 6.102.3 - 2024-05-15 -------------------- This patch improves our shrinking of unique collections, such as :func:`~hypothesis.strategies.dictionaries`, :func:`~hypothesis.strategies.sets`, and :func:`~hypothesis.strategies.lists` with ``unique=True``. .. _v6.102.2: -------------------- 6.102.2 - 2024-05-15 -------------------- This patch fixes a rare internal error when generating very large elements from strategies (:issue:`3874`). .. _v6.102.1: -------------------- 6.102.1 - 2024-05-13 -------------------- This patch fixes an overly strict internal type assertion. .. _v6.102.0: -------------------- 6.102.0 - 2024-05-13 -------------------- This release improves our support for the :pypi:`annotated-types` iterable ``GroupedMetadata`` protocol. In order to treat the elements "as if they had been unpacked", if one such element is a :class:`~hypothesis.strategies.SearchStrategy` we now resolve to that strategy. Previously, we treated this as an unknown filter predicate. We expect this to be useful for libraries implementing custom metadata - instead of requiring downstream integration, they can implement the protocol and yield a lazily-created strategy. Doing so only if Hypothesis is in :obj:`sys.modules` gives powerful integration with no runtime overhead or extra dependencies. .. _v6.101.0: -------------------- 6.101.0 - 2024-05-13 -------------------- The :func:`~hypothesis.extra.django.from_model` function currently tries to create a strategy for :obj:`~django:django.db.models.AutoField` fields if they don't have :attr:`~django:django.db.models.Field.auto_created` set to `True`. The docs say it's supposed to skip all :obj:`~django:django.db.models.AutoField` fields, so this patch updates the code to do what the docs say (:issue:`3978`). .. _v6.100.8: -------------------- 6.100.8 - 2024-05-13 -------------------- This patch adds some internal type annotations (:issue:`3074`). Thanks to Andrew Sansom for his contribution! .. _v6.100.7: -------------------- 6.100.7 - 2024-05-12 -------------------- This patch fixes a rare internal error when using :func:`~hypothesis.strategies.integers` with a high ``max_examples`` setting (:issue:`3974`). .. _v6.100.6: -------------------- 6.100.6 - 2024-05-10 -------------------- This patch improves our internal caching logic. We don't expect it to result in any performance improvements (yet!). .. _v6.100.5: -------------------- 6.100.5 - 2024-05-06 -------------------- This patch turns off a check in :func:`~hypothesis.register_random` for possibly unreferenced RNG instances on the free-threaded build of CPython 3.13 because this check has a much higher false positive rate in the free-threaded build (:issue:`3965`). Thanks to Nathan Goldbaum for this patch. .. _v6.100.4: -------------------- 6.100.4 - 2024-05-05 -------------------- This patch turns off a warning for functions decorated with :func:`typing.overload` and then :func:`~hypothesis.strategies.composite`, although only in that order (:issue:`3970`). .. _v6.100.3: -------------------- 6.100.3 - 2024-05-04 -------------------- This patch fixes a significant slowdown when using the :func:`~hypothesis.stateful.precondition` decorator in some cases, due to expensive repr formatting internally (:issue:`3963`). .. _v6.100.2: -------------------- 6.100.2 - 2024-04-28 -------------------- Explicitly cast :obj:`numpy.finfo.smallest_normal ` to builtin `float` in preparation for the :pypi:`numpy==2.0 ` release (:issue:`3950`) .. _v6.100.1: -------------------- 6.100.1 - 2024-04-08 -------------------- This patch improve a rare error message for flaky tests (:issue:`3940`). .. _v6.100.0: -------------------- 6.100.0 - 2024-03-31 -------------------- The :func:`~hypothesis.extra.numpy.from_dtype` function no longer generates ``NaT`` ("not-a-time") values for the ``datetime64`` or ``timedelta64`` dtypes if passed ``allow_nan=False`` (:issue:`3943`). .. _v6.99.13: -------------------- 6.99.13 - 2024-03-24 -------------------- This patch includes the :obj:`~hypothesis.settings.backend` setting in the ``how_generated`` field of our :ref:`observability output `. .. _v6.99.12: -------------------- 6.99.12 - 2024-03-23 -------------------- If you were running Python 3.13 (currently in alpha) with :pypi:`pytest-xdist` and then attempted to pretty-print a ``lambda`` functions which was created using the :func:`eval` builtin, it would have raised an AssertionError. Now you'll get ``"lambda ...: "``, as expected. .. _v6.99.11: -------------------- 6.99.11 - 2024-03-20 -------------------- This release improves an internal invariant. .. _v6.99.10: -------------------- 6.99.10 - 2024-03-20 -------------------- This patch fixes Hypothesis sometimes raising a ``Flaky`` error when generating collections of unique floats containing ``nan``. See :issue:`3926` for more details. .. _v6.99.9: ------------------- 6.99.9 - 2024-03-19 ------------------- This patch continues our work on refactoring the shrinker (:issue:`3921`). .. _v6.99.8: ------------------- 6.99.8 - 2024-03-18 ------------------- This patch continues our work on refactoring shrinker internals (:issue:`3921`). .. _v6.99.7: ------------------- 6.99.7 - 2024-03-18 ------------------- This release resolves :py:exc:`PermissionError` that come from creating databases on inaccessible paths. .. _v6.99.6: ------------------- 6.99.6 - 2024-03-14 ------------------- This patch starts work on refactoring our shrinker internals. There is no user-visible change. .. _v6.99.5: ------------------- 6.99.5 - 2024-03-12 ------------------- This patch fixes a longstanding performance problem in stateful testing (:issue:`3618`), where state machines which generated a substantial amount of input for each step would hit the maximum amount of entropy and then fail with an ``Unsatisfiable`` error. We now stop taking additional steps when we're approaching the entropy limit, which neatly resolves the problem without touching unaffected tests. .. _v6.99.4: ------------------- 6.99.4 - 2024-03-11 ------------------- Fix regression caused by using :pep:`696` default in TypeVar with Python 3.13.0a3. .. _v6.99.3: ------------------- 6.99.3 - 2024-03-11 ------------------- This patch further improves the type annotations in :mod:`hypothesis.extra.numpy`. .. _v6.99.2: ------------------- 6.99.2 - 2024-03-10 ------------------- Simplify the type annotation of :func:`~hypothesis.extra.pandas.column` and :func:`~hypothesis.extra.pandas.columns` by using :pep:`696` to avoid overloading. .. _v6.99.1: ------------------- 6.99.1 - 2024-03-10 ------------------- This patch implements type annotations for :func:`~hypothesis.extra.pandas.column`. .. _v6.99.0: ------------------- 6.99.0 - 2024-03-09 ------------------- This release adds the **experimental and unstable** :obj:`~hypothesis.settings.backend` setting. See :ref:`alternative-backends` for details. .. _v6.98.18: -------------------- 6.98.18 - 2024-03-09 -------------------- This patch fixes :issue:`3900`, a performance regression for :func:`~hypothesis.extra.numpy.arrays` due to the interaction of :ref:`v6.98.12` and :ref:`v6.97.1`. .. _v6.98.17: -------------------- 6.98.17 - 2024-03-04 -------------------- This patch improves the type annotations in :mod:`hypothesis.extra.numpy`, which makes inferred types more precise for both :pypi:`mypy` and :pypi:`pyright`, and fixes some strict-mode errors on the latter. Thanks to Jonathan Plasse for reporting and fixing this in :pull:`3889`! .. _v6.98.16: -------------------- 6.98.16 - 2024-03-04 -------------------- This patch paves the way for future shrinker improvements. There is no user-visible change. .. _v6.98.15: -------------------- 6.98.15 - 2024-02-29 -------------------- This release adds support for the Array API's `2023.12 release `_ via the ``api_version`` argument in :func:`~hypothesis.extra.array_api.make_strategies_namespace`. The API additions and modifications in the ``2023.12`` spec do not necessitate any changes in the Hypothesis strategies, hence there is no distinction between a ``2022.12`` and ``2023.12`` strategies namespace. .. _v6.98.14: -------------------- 6.98.14 - 2024-02-29 -------------------- This patch adjusts the printing of bundle values to correspond with their names when using stateful testing. .. _v6.98.13: -------------------- 6.98.13 - 2024-02-27 -------------------- This patch implements filter-rewriting for :func:`~hypothesis.strategies.text` and :func:`~hypothesis.strategies.binary` with the :meth:`~re.Pattern.search`, :meth:`~re.Pattern.match`, or :meth:`~re.Pattern.fullmatch` method of a :func:`re.compile`\ d regex. .. _v6.98.12: -------------------- 6.98.12 - 2024-02-25 -------------------- This patch implements filter-rewriting for most length filters on some additional collection types (:issue:`3795`), and fixes several latent bugs where unsatisfiable or partially-infeasible rewrites could trigger internal errors. .. _v6.98.11: -------------------- 6.98.11 - 2024-02-24 -------------------- This patch makes stateful testing somewhat less likely to get stuck when there are only a few possible rules. .. _v6.98.10: -------------------- 6.98.10 - 2024-02-22 -------------------- This patch :pep:`adds a note <678>` to errors which occur while drawing from a strategy, to make it easier to tell why your test failed in such cases. .. _v6.98.9: ------------------- 6.98.9 - 2024-02-20 ------------------- This patch ensures that :ref:`observability ` outputs include an informative repr for :class:`~hypothesis.stateful.RuleBasedStateMachine` stateful tests, along with more detailed timing information. .. _v6.98.8: ------------------- 6.98.8 - 2024-02-18 ------------------- This patch improves :ref:`the Ghostwriter ` for binary operators. .. _v6.98.7: ------------------- 6.98.7 - 2024-02-18 ------------------- This patch improves import-detection in :ref:`the Ghostwriter ` (:issue:`3884`), particularly for :func:`~hypothesis.strategies.from_type` and strategies from ``hypothesis.extra.*``. .. _v6.98.6: ------------------- 6.98.6 - 2024-02-15 ------------------- This patch clarifies the documentation on stateful testing (:issue:`3511`). .. _v6.98.5: ------------------- 6.98.5 - 2024-02-14 ------------------- This patch improves argument-to-json conversion for :ref:`observability ` output. Checking for a ``.to_json()`` method on the object *before* a few other options like dataclass support allows better user control of the process (:issue:`3880`). .. _v6.98.4: ------------------- 6.98.4 - 2024-02-12 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.98.3: ------------------- 6.98.3 - 2024-02-08 ------------------- This patch fixes an error when generating :ref:`observability ` reports involving large (``n > 1e308``) integers. .. _v6.98.2: ------------------- 6.98.2 - 2024-02-05 ------------------- This patch refactors some internals. There is no user-visible change. .. _v6.98.1: ------------------- 6.98.1 - 2024-02-05 ------------------- This release improves our distribution of generated values for all strategies, by doing a better job of tracking which values we have generated before and avoiding generating them again. For example, ``st.lists(st.integers())`` previously generated ~5 each of ``[]`` ``[0]`` in 100 examples. In this release, each of ``[]`` and ``[0]`` are generated ~1-2 times each. .. _v6.98.0: ------------------- 6.98.0 - 2024-02-05 ------------------- This release deprecates use of the global random number generator while drawing from a strategy, because this makes test cases less diverse and prevents us from reporting minimal counterexamples (:issue:`3810`). If you see this new warning, you can get a quick fix by using :func:`~hypothesis.strategies.randoms`; or use more idiomatic strategies :func:`~hypothesis.strategies.sampled_from`, :func:`~hypothesis.strategies.floats`, :func:`~hypothesis.strategies.integers`, and so on. Note that the same problem applies to e.g. ``numpy.random``, but for performance reasons we only check the stdlib :mod:`random` module - ignoring even other sources passed to :func:`~hypothesis.register_random`. .. _v6.97.6: ------------------- 6.97.6 - 2024-02-04 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.97.5: ------------------- 6.97.5 - 2024-02-03 ------------------- This patch adds some :ref:`observability information ` about how many times predicates in :func:`~hypothesis.assume` or :func:`~hypothesis.stateful.precondition` were satisfied, so that downstream tools can warn you if some were *never* satisfied by any test case. .. _v6.97.4: ------------------- 6.97.4 - 2024-01-31 ------------------- This patch improves formatting and adds some cross-references to our docs. .. _v6.97.3: ------------------- 6.97.3 - 2024-01-30 ------------------- Internal test refactoring. .. _v6.97.2: ------------------- 6.97.2 - 2024-01-30 ------------------- This patch slightly changes how we replay examples from :ref:`the database `: if the behavior of the saved example has changed, we now keep running the test case instead of aborting at the size of the saved example. While we know it's not the *same* example, we might as well continue running the test! Because we now finish running a few more examples for affected tests, this might be a slight slowdown - but correspondingly more likely to find a bug. We've also applied similar tricks to the |Phase.target| phase, where they are a pure performance improvement for affected tests. .. _v6.97.1: ------------------- 6.97.1 - 2024-01-27 ------------------- Improves the performance of the :func:`~hypothesis.extra.numpy.arrays` strategy when generating unique values. .. _v6.97.0: ------------------- 6.97.0 - 2024-01-25 ------------------- Changes the distribution of :func:`~hypothesis.strategies.sampled_from` when sampling from a :class:`~python:enum.Flag`. Previously, no-flags-set values would never be generated, and all-flags-set values would be unlikely for large enums. With this change, the distribution is more uniform in the number of flags set. .. _v6.96.4: ------------------- 6.96.4 - 2024-01-23 ------------------- This patch slightly refactors some internals. There is no user-visible change. .. _v6.96.3: ------------------- 6.96.3 - 2024-01-22 ------------------- This patch fixes a spurious warning about slow imports when ``HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY`` was set. .. _v6.96.2: ------------------- 6.96.2 - 2024-01-21 ------------------- This patch refactors some more internals, continuing our work on supporting alternative backends (:issue:`3086`). There is no user-visible change. .. _v6.96.1: ------------------- 6.96.1 - 2024-01-18 ------------------- Fix a spurious warning seen when running pytest's test suite, caused by never realizing we got out of initialization due to imbalanced hook calls. .. _v6.96.0: ------------------- 6.96.0 - 2024-01-17 ------------------- Warns when constructing a `repr` that is overly long. This can happen by accident if stringifying arbitrary strategies, and is expensive in time and memory. The associated deferring of these long strings in :func:`~hypothesis.strategies.sampled_from` should also lead to improved performance. .. _v6.95.0: ------------------- 6.95.0 - 2024-01-17 ------------------- This release adds the ability to pass any object to :func:`~hypothesis.note`, instead of just strings. The pretty-printed representation of the object will be used. See also :issue:`3843`. .. _v6.94.0: ------------------- 6.94.0 - 2024-01-16 ------------------- This release avoids creating a ``.hypothesis`` directory when using :func:`~hypothesis.strategies.register_type_strategy` (:issue:`3836`), and adds warnings for plugins which do so by other means or have other unintended side-effects. .. _v6.93.2: ------------------- 6.93.2 - 2024-01-15 ------------------- This patch improves :ref:`observability ` reports by moving timing information from ``metadata`` to a new ``timing`` key, and supporting conversion of additional argument types to json rather than string reprs via a ``.to_json()`` method (including e.g. Pandas dataframes). Additionally, the :obj:`~hypothesis.HealthCheck.too_slow` health check will now report *which* strategies were slow, e.g. for strategies a, b, c, ...:: count | fraction | slowest draws (seconds) a | 3 | 65% | -- -- -- 0.357, 2.000 b | 8 | 16% | 0.100, 0.100, 0.100, 0.111, 0.123 c | 3 | 8% | -- -- 0.030, 0.050, 0.200 (skipped 2 rows of fast draws) .. _v6.93.1: ------------------- 6.93.1 - 2024-01-15 ------------------- This patch refactors some internals, continuing our work on supporting alternative backends (:issue:`3086`). There is no user-visible change. .. _v6.93.0: ------------------- 6.93.0 - 2024-01-13 ------------------- The :func:`~hypothesis.extra.lark.from_lark` strategy now accepts an ``alphabet=`` argument, which is passed through to :func:`~hypothesis.strategies.from_regex`, so that you can e.g. constrain the generated strings to a particular codec. In support of this feature, :func:`~hypothesis.strategies.from_regex` will avoid generating optional parts which do not fit the alphabet. For example, ``from_regex(r"abc|def", alphabet="abcd")`` was previously an error, and will now generate only ``'abc'``. Cases where there are no valid strings remain an error. .. _v6.92.9: ------------------- 6.92.9 - 2024-01-12 ------------------- This patch refactors some internals, continuing our work on supporting alternative backends (:issue:`3086`). There is no user-visible change. .. _v6.92.8: ------------------- 6.92.8 - 2024-01-11 ------------------- This patch adds a :ref:`test statistics ` event when a generated example is rejected via :func:`assume `. This may also help with distinguishing ``gave_up`` examples in :ref:`observability ` (:issue:`3827`). .. _v6.92.7: ------------------- 6.92.7 - 2024-01-10 ------------------- This introduces the rewriting of length filters on some collection strategies (:issue:`3791`). Thanks to Reagan Lee for implementing this feature! .. _v6.92.6: ------------------- 6.92.6 - 2024-01-08 ------------------- If a test uses :func:`~hypothesis.strategies.sampled_from` on a sequence of strategies, and raises a ``TypeError``, we now :pep:`add a note <678>` asking whether you meant to use :func:`~hypothesis.strategies.one_of`. Thanks to Vince Reuter for suggesting and implementing this hint! .. _v6.92.5: ------------------- 6.92.5 - 2024-01-08 ------------------- This patch registers explicit strategies for a handful of builtin types, motivated by improved introspection in PyPy 7.3.14 triggering existing internal warnings. Thanks to Carl Friedrich Bolz-Tereick for helping us work out what changed! .. _v6.92.4: ------------------- 6.92.4 - 2024-01-08 ------------------- This patch fixes an error when writing :ref:`observability ` reports without a pre-existing ``.hypothesis`` directory. .. _v6.92.3: ------------------- 6.92.3 - 2024-01-08 ------------------- This patch adds a new environment variable ``HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY_NOCOVER``, which turns on :ref:`observability ` data collection without collecting code coverage data, which may be faster on Python 3.11 and earlier. Thanks to Harrison Goldstein for reporting and fixing :issue:`3821`. .. _v6.92.2: ------------------- 6.92.2 - 2023-12-27 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.92.1: ------------------- 6.92.1 - 2023-12-16 ------------------- This patch fixes a bug introduced in :ref:`version 6.92.0 `, where using the :func:`~hypothesis.strategies.data` strategy would fail to draw a :func:`~python:dataclasses.dataclass` with a :class:`~python:collections.defaultdict` field. This was due to a bug in the standard library which `was fixed in 3.12 `__, so we've vendored the fix. .. _v6.92.0: ------------------- 6.92.0 - 2023-12-10 ------------------- This release adds an experimental :wikipedia:`observability ` mode. :ref:`You can read the docs about it here `. .. _v6.91.2: ------------------- 6.91.2 - 2023-12-10 ------------------- This patch refactors some more internals, continuing our work on supporting alternative backends (:issue:`3086`). There is no user-visible change. .. _v6.91.1: ------------------- 6.91.1 - 2023-12-08 ------------------- This patch fixes an issue where :func:`~hypothesis.strategies.builds` could not be used with :pypi:`attrs` objects that defined private attributes (i.e. attributes with a leading underscore). See also :issue:`3791`. This patch also adds support more generally for using :func:`~hypothesis.strategies.builds` with attrs' ``alias`` parameter, which was previously unsupported. This patch increases the minimum required version of attrs to 22.2.0. .. _v6.91.0: ------------------- 6.91.0 - 2023-11-27 ------------------- This release adds an optional ``payload`` argument to :func:`hypothesis.event`, so that you can clearly express the difference between the label and the value of an observation. :ref:`statistics` will still summarize it as a string, but future observability options can preserve the distinction. .. _v6.90.1: ------------------- 6.90.1 - 2023-11-27 ------------------- This patch supports assigning ``settings = settings(...)`` as a class attribute on a subclass of a ``.TestCase`` attribute of a :class:`~hypothesis.stateful.RuleBasedStateMachine`. Previously, this did nothing at all. .. code-block:: python # works as of this release class TestMyStatefulMachine(MyStatefulMachine.TestCase): settings = settings(max_examples=10000) # the old way still works, but it's more verbose. MyStateMachine.TestCase.settings = settings(max_examples=10000) class TestMyStatefulMachine(MyStatefulMachine.TestCase): pass Thanks to Joey Tran for reporting these settings-related edge cases in stateful testing. .. _v6.90.0: ------------------- 6.90.0 - 2023-11-20 ------------------- This release makes it an error to assign ``settings = settings(...)`` as a class attribute on a :class:`~hypothesis.stateful.RuleBasedStateMachine`. This has never had any effect, and it should be used as a decorator instead: .. code-block:: python class BadMachine(RuleBasedStateMachine): """This doesn't do anything, and is now an error!""" settings = settings(derandomize=True) @settings(derandomize=True) class GoodMachine(RuleBasedStateMachine): """This is the right way to do it :-)""" .. _v6.89.1: ------------------- 6.89.1 - 2023-11-19 ------------------- This patch refactors some internals. There is no user-visible change, but we hope to improve performance and unlock support for alternative backends such as :pypi:`symbolic execution with crosshair ` in future (:issue:`3086`). Thanks to Liam DeVoe for this fantastic contribution! .. _v6.89.0: ------------------- 6.89.0 - 2023-11-16 ------------------- This release teaches :func:`~hypothesis.strategies.from_type` to handle constraints implied by the :pypi:`annotated-types` package - as used by e.g. :pypi:`pydantic`. This is usually efficient, but falls back to filtering in a few remaining cases. Thanks to Viicos for :pull:`3780`! .. _v6.88.4: ------------------- 6.88.4 - 2023-11-13 ------------------- This patch adds a warning when :func:`@st.composite ` wraps a function annotated as returning a :class:`~hypothesis.strategies.SearchStrategy`, since this is usually an error (:issue:`3786`). The function should return a value, and the decorator will convert it to a function which returns a strategy. .. _v6.88.3: ------------------- 6.88.3 - 2023-11-05 ------------------- This patch refactors ``from_type(typing.Tuple)``, allowing :func:`~hypothesis.strategies.register_type_strategy` to take effect for tuples instead of being silently ignored (:issue:`3750`). Thanks to Nick Collins for reporting and extensive work on this issue. .. _v6.88.2: ------------------- 6.88.2 - 2023-11-05 ------------------- This patch improves the speed of the explain phase on python 3.12+, by using the new :mod:`sys.monitoring` module to collect coverage, instead of :obj:`sys.settrace`. Thanks to Liam DeVoe for :pull:`3776`! .. _v6.88.1: ------------------- 6.88.1 - 2023-10-16 ------------------- This patch improves :func:`~hypothesis.strategies.register_type_strategy` when used with ``tuple`` subclasses, by preventing them from being interpreted as generic and provided to strategies like ``st.from_type(Sequence[int])`` (:issue:`3767`). .. _v6.88.0: ------------------- 6.88.0 - 2023-10-15 ------------------- This release allows strategy-generating functions registered with :func:`~hypothesis.strategies.register_type_strategy` to conditionally not return a strategy, by returning :data:`NotImplemented` (:issue:`3767`). .. _v6.87.4: ------------------- 6.87.4 - 2023-10-12 ------------------- When :func:`~hypothesis.strategies.randoms` was called with ``use_true_randoms=False``, calling ``r.sample([], 0)`` would result in an error, when it should have returned an empty sequence to agree with the normal behaviour of :func:`random.sample`. This fixes that discrepancy (:issue:`3765`). .. _v6.87.3: ------------------- 6.87.3 - 2023-10-06 ------------------- This patch ensures that the :ref:`hypothesis codemod ` CLI will print a warning instead of stopping with an internal error if one of your files contains invalid syntax (:issue:`3759`). .. _v6.87.2: ------------------- 6.87.2 - 2023-10-06 ------------------- This patch makes some small changes to our NumPy integration to ensure forward compatibility. Thanks to Mateusz Sokół for :pull:`3761`. .. _v6.87.1: ------------------- 6.87.1 - 2023-10-01 ------------------- Fixes :issue:`3755`, where an internal condition turns out to be reachable after all. .. _v6.87.0: ------------------- 6.87.0 - 2023-09-25 ------------------- This release deprecates use of :func:`~hypothesis.assume` and ``reject()`` outside of property-based tests, because these functions work by raising a special exception (:issue:`3743`). It also fixes some type annotations (:issue:`3753`). .. _v6.86.2: ------------------- 6.86.2 - 2023-09-18 ------------------- Hotfix for :issue:`3747`, a bug in explain mode which is so rare that we missed it in six months of dogfooding. Thanks to :pypi:`mygrad` for discovering and promptly reporting this! .. _v6.86.1: ------------------- 6.86.1 - 2023-09-17 ------------------- This patch improves the documentation of :obj:`@example(...).xfail() ` by adding a note about :pep:`614`, similar to :obj:`@example(...).via() `, and adds a warning when a strategy generates a test case which seems identical to one provided by an xfailed example. .. _v6.86.0: ------------------- 6.86.0 - 2023-09-17 ------------------- This release enables the |Phase.explain| phase by default. We hope it helps you to understand *why* your failing tests have failed! .. _v6.85.1: ------------------- 6.85.1 - 2023-09-16 ------------------- This patch switches some of our type annotations to use :obj:`typing.Literal` when only a few specific values are allowed, such as UUID or IP address versions. .. _v6.85.0: ------------------- 6.85.0 - 2023-09-16 ------------------- This release deprecates the old whitelist/blacklist arguments to :func:`~hypothesis.strategies.characters`, in favor of include/exclude arguments which more clearly describe their effects on the set of characters which can be generated. You can :ref:`use Hypothesis' codemods ` to automatically upgrade to the new argument names. In a future version, the old names will start to raise a ``DeprecationWarning``. .. _v6.84.3: ------------------- 6.84.3 - 2023-09-10 ------------------- This patch automatically disables the :obj:`~hypothesis.HealthCheck.differing_executors` health check for methods which are also pytest parametrized tests, because those were mostly false alarms (:issue:`3733`). .. _v6.84.2: ------------------- 6.84.2 - 2023-09-06 ------------------- Building on recent releases, :func:`~hypothesis.strategies.characters` now accepts _any_ ``codec=``, not just ``"utf-8"`` and ``"ascii"``. This includes standard codecs from the :mod:`codecs` module and their aliases, platform specific and user-registered codecs if they are available, and `python-specific text encodings `__ (but not text transforms or binary transforms). .. _v6.84.1: ------------------- 6.84.1 - 2023-09-05 ------------------- This patch by Reagan Lee makes ``st.text(...).filter(str.isidentifier)`` return an efficient custom strategy (:issue:`3480`). .. _v6.84.0: ------------------- 6.84.0 - 2023-09-04 ------------------- The :func:`~hypothesis.strategies.from_regex` strategy now takes an optional ``alphabet=characters(codec="utf-8")`` argument for unicode strings, like :func:`~hypothesis.strategies.text`. This offers more and more-consistent control over the generated strings, removing previously-hard-coded limitations. With ``fullmatch=False`` and ``alphabet=characters()``, surrogate characters are now possible in leading and trailing text as well as the body of the match. Negated character classes such as ``[^A-Z]`` or ``\S`` had a hard-coded exclusion of control characters and surrogate characters; now they permit anything in ``alphabet=`` consistent with the class, and control characters are permitted by default. .. _v6.83.2: ------------------- 6.83.2 - 2023-09-04 ------------------- Add a health check that detects if the same test is executed several times by :ref:`different executors`. This can lead to difficult-to-debug problems such as :issue:`3446`. .. _v6.83.1: ------------------- 6.83.1 - 2023-09-03 ------------------- Pretty-printing of failing examples can now use functions registered with :func:`IPython.lib.pretty.for_type` or :func:`~IPython.lib.pretty.for_type_by_name`, as well as restoring compatibility with ``_repr_pretty_`` callback methods which were accidentally broken in :ref:`version 6.61.2 ` (:issue:`3721`). .. _v6.83.0: ------------------- 6.83.0 - 2023-09-01 ------------------- Adds a new ``codec=`` option in :func:`~hypothesis.strategies.characters`, making it convenient to produce only characters which can be encoded as ``ascii`` or ``utf-8`` bytestrings. Support for other codecs will be added in a future release. .. _v6.82.7: ------------------- 6.82.7 - 2023-08-28 ------------------- This patch updates our autoformatting tools, improving our code style without any API changes. .. _v6.82.6: ------------------- 6.82.6 - 2023-08-20 ------------------- This patch enables and fixes many more of :pypi:`ruff`\ 's lint rules. .. _v6.82.5: ------------------- 6.82.5 - 2023-08-18 ------------------- Fixes the error message for missing ``[cli]`` extra. .. _v6.82.4: ------------------- 6.82.4 - 2023-08-12 ------------------- This patch ensures that we always close the download connection in :class:`~hypothesis.database.GitHubArtifactDatabase`. .. _v6.82.3: ------------------- 6.82.3 - 2023-08-08 ------------------- We can now pretty-print combinations of *zero* :class:`enum.Flag` values, like ``SomeFlag(0)``, which has never worked before. .. _v6.82.2: ------------------- 6.82.2 - 2023-08-06 ------------------- This patch fixes pretty-printing of combinations of :class:`enum.Flag` values, which was previously an error (:issue:`3709`). .. _v6.82.1: ------------------- 6.82.1 - 2023-08-05 ------------------- Improve shrinking of floats in narrow regions that don't cross an integer boundary. Closes :issue:`3357`. .. _v6.82.0: ------------------- 6.82.0 - 2023-07-20 ------------------- :func:`~hypothesis.strategies.from_regex` now supports the atomic grouping (``(?>...)``) and possessive quantifier (``*+``, ``++``, ``?+``, ``{m,n}+``) syntax `added in Python 3.11 `__. Thanks to Cheuk Ting Ho for implementing this! .. _v6.81.2: ------------------- 6.81.2 - 2023-07-15 ------------------- If the :envvar:`HYPOTHESIS_NO_PLUGINS` environment variable is set, we'll avoid :ref:`loading plugins ` such as `the old Pydantic integration `__ or `HypoFuzz' CLI options `__. This is probably only useful for our own self-tests, but documented in case it might help narrow down any particularly weird bugs in complex environments. .. _v6.81.1: ------------------- 6.81.1 - 2023-07-11 ------------------- Fixes some lingering issues with inference of recursive types in :func:`~hypothesis.strategies.from_type`. Closes :issue:`3525`. .. _v6.81.0: ------------------- 6.81.0 - 2023-07-10 ------------------- This release further improves our ``.patch``-file support from :ref:`version 6.75 `, skipping duplicates, tests which use :func:`~hypothesis.strategies.data` (and don't support :obj:`@example() `\ ), and various broken edge-cases. Because :pypi:`libCST ` has released version 1.0 which uses the native parser by default, we no longer set the ``LIBCST_PARSER_TYPE=native`` environment variable. If you are using an older version, you may need to upgrade or set this envvar for yourself. .. _v6.80.1: ------------------- 6.80.1 - 2023-07-06 ------------------- This patch updates some internal code for selftests. There is no user-visible change. .. _v6.80.0: ------------------- 6.80.0 - 2023-06-27 ------------------- This release drops support for Python 3.7, `which reached end of life on 2023-06-27 `__. .. _v6.79.4: ------------------- 6.79.4 - 2023-06-27 ------------------- Fixes occasional recursion-limit-exceeded errors when validating deeply nested strategies. Closes: :issue:`3671` .. _v6.79.3: ------------------- 6.79.3 - 2023-06-26 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.79.2: ------------------- 6.79.2 - 2023-06-22 ------------------- Improve the type rendered in :func:`~hypothesis.strategies.from_type`, which improves the coverage of Ghostwriter. .. _v6.79.1: ------------------- 6.79.1 - 2023-06-19 ------------------- We now test against Python 3.12 beta in CI, and this patch fixes some new deprecations. .. _v6.79.0: ------------------- 6.79.0 - 2023-06-17 ------------------- This release changes :func:`~hypothesis.strategies.register_type_strategy` for compatibility with :pep:`585`: we now store only a single strategy or resolver function which is used for both the builtin and the ``typing`` module version of each type (:issue:`3635`). If you previously relied on registering separate strategies for e.g. ``list`` vs ``typing.List``, you may need to use explicit strategies rather than inferring them from types. .. _v6.78.3: ------------------- 6.78.3 - 2023-06-15 ------------------- This release ensures that Ghostwriter does not use the deprecated aliases for the ``collections.abc`` classes in ``collections``. .. _v6.78.2: ------------------- 6.78.2 - 2023-06-13 ------------------- This patch improves Ghostwriter's use of qualified names for re-exported functions and classes, and avoids importing useless :obj:`~typing.TypeVar`\ s. .. _v6.78.1: ------------------- 6.78.1 - 2023-06-12 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.78.0: ------------------- 6.78.0 - 2023-06-11 ------------------- New input validation for :func:`~hypothesis.strategies.recursive` will raise an error rather than hanging indefinitely if passed invalid ``max_leaves=`` arguments. .. _v6.77.0: ------------------- 6.77.0 - 2023-06-09 ------------------- :func:`~hypothesis.strategies.from_type` now handles numpy array types: :obj:`np.typing.ArrayLike `, :obj:`np.typing.NDArray `, and parameterized versions including :class:`np.ndarray[shape, elem_type] `. .. _v6.76.0: ------------------- 6.76.0 - 2023-06-04 ------------------- Warn in :func:`~hypothesis.strategies.from_type` if the inferred strategy has no variation (always returning default instances). Also handles numpy data types by calling :func:`~hypothesis.extra.numpy.from_dtype` on the corresponding dtype, thus ensuring proper variation for these types. .. _v6.75.9: ------------------- 6.75.9 - 2023-05-31 ------------------- :func:`~hypothesis.strategies.from_type` now works in cases where we use :func:`~hypothesis.strategies.builds` to create an instance and the constructor has an argument which would lead to recursion. Previously, this would raise an error if the argument had a default value. Thanks to Joachim B Haga for reporting and fixing this problem. .. _v6.75.8: ------------------- 6.75.8 - 2023-05-31 ------------------- In preparation for supporting JAX in :ref:`hypothesis.extra.array_api `, this release supports immutable arrays being generated via :func:`xps.arrays`. In particular, we internally removed an instance of in-place array modification, which isn't possible for an immutable array. .. _v6.75.7: ------------------- 6.75.7 - 2023-05-30 ------------------- This release fixes some ``.patch``-file bugs from :ref:`version 6.75 `, and adds automatic support for writing ``@hypothesis.example()`` or ``@example()`` depending on the current style in your test file - defaulting to the latter. Note that this feature requires :pypi:`libcst` to be installed, and :pypi:`black` is strongly recommended. You can ensure you have the dependencies with ``pip install "hypothesis[cli,codemods]"``. .. _v6.75.6: ------------------- 6.75.6 - 2023-05-27 ------------------- This patch continues the work started in :pull:`3651` by adding :pypi:`ruff` linter rules for :pypi:`pyflakes`, :pypi:`flake8-comprehensions`, and :pypi:`flake8-implicit-str-concat`. .. _v6.75.5: ------------------- 6.75.5 - 2023-05-26 ------------------- This patch updates our linter stack to use :pypi:`ruff`, and fixes some previously-ignored lints. Thanks to Christian Clauss for his careful review and :pull:`3651`! .. _v6.75.4: ------------------- 6.75.4 - 2023-05-26 ------------------- Hypothesis will now record an event for more cases where data is marked invalid, including for exceeding the internal depth limit. .. _v6.75.3: ------------------- 6.75.3 - 2023-05-14 ------------------- This patch fixes :func:`~hypothesis.strategies.complex_numbers` accidentally invalidating itself when passed magnitude arguments for 32 and 64-bit widths, i.e. 16- and 32-bit floats, due to not internally down-casting numbers (:issue:`3573`). .. _v6.75.2: ------------------- 6.75.2 - 2023-05-04 ------------------- Improved the documentation regarding how to use :class:`~hypothesis.database.GitHubArtifactDatabase` and fixed a bug that occurred in repositories with no existing artifacts. Thanks to Agustín Covarrubias for this contribution. .. _v6.75.1: ------------------- 6.75.1 - 2023-04-30 ------------------- ``hypothesis.errors`` will now raise :py:exc:`AttributeError` when attempting to access an undefined attribute, rather than returning :py:obj:`None`. .. _v6.75.0: ------------------- 6.75.0 - 2023-04-30 ------------------- Sick of adding :obj:`@example() `\ s by hand? Our Pytest plugin now writes ``.patch`` files to insert them for you, making `this workflow `__ easier than ever before. Note that you'll need :pypi:`LibCST ` (via :ref:`codemods`), and that :obj:`@example().via() ` requires :pep:`614` (Python 3.9 or later). .. _v6.74.1: ------------------- 6.74.1 - 2023-04-28 ------------------- This patch provides better error messages for datetime- and timedelta-related invalid dtypes in our Pandas extra (:issue:`3518`). Thanks to Nick Muoh at the PyCon Sprints! .. _v6.74.0: ------------------- 6.74.0 - 2023-04-26 ------------------- This release adds support for `nullable pandas dtypes `__ in :func:`~hypothesis.extra.pandas` (:issue:`3604`). Thanks to Cheuk Ting Ho for implementing this at the PyCon sprints! .. _v6.73.1: ------------------- 6.73.1 - 2023-04-27 ------------------- This patch updates our minimum Numpy version to 1.16, and restores compatibility with versions before 1.20, which were broken by a mistake in Hypothesis 6.72.4 (:issue:`3625`). .. _v6.73.0: ------------------- 6.73.0 - 2023-04-25 ------------------- This release upgrades the |Phase.explain| phase (:issue:`3411`). * Following the first failure, Hypothesis will (usually, depending on the enabled |Phase|) track which lines of code were executed by passing and failing examples, and report where they diverged - with some heuristics to drop unhelpful reports. This is an existing feature, now upgraded and newly enabled by default. * After shrinking to a minimal failing example, Hypothesis will try to find parts of the example -- e.g. separate args to :func:`@given() ` -- which can vary freely without changing the result of that minimal failing example. If the automated experiments run without finding a passing variation, we leave a comment in the final report: .. code-block:: python test_x_divided_by_y( x=0, # or any other generated value y=0, ) Just remember that the *lack* of an explanation sometimes just means that Hypothesis couldn't efficiently find one, not that no explanation (or simpler failing example) exists. .. _v6.72.4: ------------------- 6.72.4 - 2023-04-25 ------------------- This patch fixes type annotations for the :func:`~hypothesis.extra.numpy.arrays` strategy. Thanks to Francesc Elies for :pull:`3602`. .. _v6.72.3: ------------------- 6.72.3 - 2023-04-25 ------------------- This patch fixes a bug with :func:`~hypothesis.strategies.from_type()` with ``dict[tuple[int, int], str]`` (:issue:`3527`). Thanks to Nick Muoh at the PyCon Sprints! .. _v6.72.2: ------------------- 6.72.2 - 2023-04-24 ------------------- This patch refactors our internals to facilitate an upcoming feature. .. _v6.72.1: ------------------- 6.72.1 - 2023-04-19 ------------------- This patch fixes some documentation and prepares for future features. .. _v6.72.0: ------------------- 6.72.0 - 2023-04-16 ------------------- This release deprecates ``Healthcheck.all()``, and :ref:`adds a codemod ` to automatically replace it with ``list(Healthcheck)`` (:issue:`3596`). .. _v6.71.0: ------------------- 6.71.0 - 2023-04-07 ------------------- This release adds :class:`~hypothesis.database.GitHubArtifactDatabase`, a new database backend that allows developers to access the examples found by a Github Actions CI job. This is particularly useful for workflows that involve continuous fuzzing, like `HypoFuzz `__. Thanks to Agustín Covarrubias for this feature! .. _v6.70.2: ------------------- 6.70.2 - 2023-04-03 ------------------- This patch clarifies the reporting of time spent generating data. A simple arithmetic mean of the percentage of time spent can be misleading; reporting the actual time spent avoids misunderstandings. Thanks to Andrea Reina for reporting and fixing :issue:`3598`! .. _v6.70.1: ------------------- 6.70.1 - 2023-03-27 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.70.0: ------------------- 6.70.0 - 2023-03-16 ------------------- This release adds an optional ``domains=`` parameter to the :func:`~hypothesis.strategies.emails` strategy, and excludes the special-use :wikipedia:`.arpa` domain from the default strategy (:issue:`3567`). Thanks to Jens Tröger for reporting and fixing this bug! .. _v6.69.0: ------------------- 6.69.0 - 2023-03-15 ------------------- This release turns ``HealthCheck.return_value`` and ``HealthCheck.not_a_test_method`` into unconditional errors. Passing them to ``suppress_health_check=`` is therefore a deprecated no-op. (:issue:`3568`). Thanks to Reagan Lee for the patch! Separately, GraalPy can now run and pass most of the hypothesis test suite (:issue:`3587`). .. _v6.68.3: ------------------- 6.68.3 - 2023-03-15 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.68.2: ------------------- 6.68.2 - 2023-02-17 ------------------- This patch fixes missing imports of the :mod:`re` module, when :ref:`ghostwriting ` tests which include compiled patterns or regex flags. Thanks to Jens Heinrich for reporting and promptly fixing this bug! .. _v6.68.1: ------------------- 6.68.1 - 2023-02-12 ------------------- This patch adds some private hooks for use in research on `Schemathesis `__ (`see our preprint here `__). .. _v6.68.0: ------------------- 6.68.0 - 2023-02-09 ------------------- This release adds support for the Array API's `2022.12 release `_ via the ``api_version`` argument in :func:`~hypothesis.extra.array_api.make_strategies_namespace`. Concretely this involves complex support in its existing strategies, plus an introduced :func:`xps.complex_dtypes` strategy. Additionally this release now treats :ref:`hypothesis.extra.array_api ` as stable, meaning breaking changes should only happen with major releases of Hypothesis. .. _v6.67.1: ------------------- 6.67.1 - 2023-02-05 ------------------- This patch updates our autoformatting tools, improving our code style without any API changes. .. _v6.67.0: ------------------- 6.67.0 - 2023-02-05 ------------------- This release allows for more precise generation of complex numbers using :func:`~hypothesis.extra.numpy.from_dtype`, by supporting the ``width``, ``min_magnitude``, and ``min_magnitude`` arguments (:issue:`3468`). Thanks to Felix Divo for this feature! .. _v6.66.2: ------------------- 6.66.2 - 2023-02-04 ------------------- This patch fixes a rare ``RecursionError`` when pretty-printing a multi-line object without type-specific printer, which was passed to a function which returned the same object by ``.map()`` or :func:`~hypothesis.strategies.builds` and thus recursed due to the new pretty reprs in Hypothesis :ref:`v6.65.0` (:issue:`3560`). Apologies to all those affected. .. _v6.66.1: ------------------- 6.66.1 - 2023-02-03 ------------------- This makes :func:`~hypothesis.extra.numpy.from_dtype` pass through the parameter ``allow_subnormal`` for complex dtypes. .. _v6.66.0: ------------------- 6.66.0 - 2023-02-02 ------------------- This release adds a ``width`` parameter to :func:`~hypothesis.strategies.complex_numbers`, analogously to :func:`~hypothesis.strategies.floats`. Thanks to Felix Divo for the new feature! .. _v6.65.2: ------------------- 6.65.2 - 2023-01-27 ------------------- This patch fixes invalid annotations detected for the tests generated by :ref:`Ghostwriter `. It will now correctly generate ``Optional`` types with just one type argument and handle union expressions inside of type arguments correctly. Additionally, it now supports code with the ``from __future__ import annotations`` marker for Python 3.10 and newer. .. _v6.65.1: ------------------- 6.65.1 - 2023-01-26 ------------------- This release improves the pretty-printing of enums in falsifying examples, so that they print as their full identifier rather than their repr. .. _v6.65.0: ------------------- 6.65.0 - 2023-01-24 ------------------- Hypothesis now reports some failing inputs by showing the call which constructed an object, rather than the repr of the object. This can be helpful when the default repr does not include all relevant details, and will unlock further improvements in a future version. For now, we capture calls made via :func:`~hypothesis.strategies.builds`, and via |.map|. .. _v6.64.0: ------------------- 6.64.0 - 2023-01-23 ------------------- The :ref:`Ghostwriter ` will now include type annotations on tests for type-annotated code. If you want to force this to happen (or not happen), pass a boolean to the new ``annotate=`` argument to the Python functions, or the ``--[no-]annotate`` CLI flag. Thanks to Nicolas Ganz for this new feature! .. _v6.63.0: ------------------- 6.63.0 - 2023-01-20 ------------------- :func:`~hypothesis.extra.pandas.range_indexes` now accepts a ``name=`` argument, to generate named :class:`pandas.RangeIndex` objects. Thanks to Sam Watts for this new feature! .. _v6.62.1: ------------------- 6.62.1 - 2023-01-14 ------------------- This patch tweaks :func:`xps.arrays` internals to improve PyTorch compatibility. Specifically, ``torch.full()`` does not accept integers as the shape argument (n.b. technically "size" in torch), but such behaviour is expected in internal code, so we copy the ``torch`` module and patch in a working ``full()`` function. .. _v6.62.0: ------------------- 6.62.0 - 2023-01-08 ------------------- A classic error when testing is to write a test function that can never fail, even on inputs that aren't allowed or manually provided. By analogy to the design pattern of:: @pytest.mark.parametrize("arg", [ ..., # passing examples pytest.param(..., marks=[pytest.mark.xfail]) # expected-failing input ]) we now support :obj:`@example(...).xfail() `, with the same (optional) ``condition``, ``reason``, and ``raises`` arguments as ``pytest.mark.xfail()``. Naturally you can also write ``.via(...).xfail(...)``, or ``.xfail(...).via(...)``, if you wish to note the provenance of expected-failing examples. .. _v6.61.3: ------------------- 6.61.3 - 2023-01-08 ------------------- This patch teaches our enhanced :func:`~typing.get_type_hints` function to 'see through' :obj:`~functools.partial` application, allowing inference from type hints to work in a few more cases which aren't (yet!) supported by the standard-library version. .. _v6.61.2: ------------------- 6.61.2 - 2023-01-07 ------------------- This patch improves our pretty-printing of failing examples, including some refactoring to prepare for exciting future features. .. _v6.61.1: ------------------- 6.61.1 - 2023-01-06 ------------------- This patch brings our :func:`~hypothesis.provisional.domains` and :func:`~hypothesis.strategies.emails` strategies into compliance with :rfc:`RFC 5890 §2.3.1 <5890>`: we no longer generate parts-of-domains where the third and fourth characters are ``--`` ("R-LDH labels"), though future versions *may* deliberately generate ``xn--`` punycode labels. Thanks to :pypi:`python-email-validator` for `the report `__! .. _v6.61.0: ------------------- 6.61.0 - 2022-12-11 ------------------- This release improves our treatment of database keys, which based on (among other things) the source code of your test function. We now post-process this source to ignore decorators, comments, trailing whitespace, and blank lines - so that you can add :obj:`@example() `\ s or make some small no-op edits to your code without preventing replay of any known failing or covering examples. .. _v6.60.1: ------------------- 6.60.1 - 2022-12-11 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.60.0: ------------------- 6.60.0 - 2022-12-04 ------------------- This release improves Hypothesis' ability to resolve forward references in type annotations. It fixes a bug that prevented :func:`~hypothesis.strategies.builds` from being used with `pydantic models that possess updated forward references `__. See :issue:`3519`. .. _v6.59.0: ------------------- 6.59.0 - 2022-12-02 ------------------- The :obj:`@example(...) ` decorator now has a ``.via()`` method, which future tools will use to track automatically-added covering examples (:issue:`3506`). .. _v6.58.2: ------------------- 6.58.2 - 2022-11-30 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.58.1: ------------------- 6.58.1 - 2022-11-26 ------------------- This patch shifts ``hypothesis[lark]`` from depending on the old :pypi:`lark-parser` package to the new :pypi:`lark` package. There are no code changes in Hypothesis, it's just that Lark got a new name on PyPI for version 1.0 onwards. .. _v6.58.0: ------------------- 6.58.0 - 2022-11-19 ------------------- :func:`~hypothesis.register_random` has used :mod:`weakref` since :ref:`v6.27.1`, allowing the :class:`~random.Random`-compatible objects to be garbage-collected when there are no other references remaining in order to avoid memory leaks. We now raise an error or emit a warning when this seems likely to happen immediately. The type annotation of :func:`~hypothesis.register_random` was also widened so that structural subtypes of :class:`~random.Random` are accepted by static typecheckers. .. _v6.57.1: ------------------- 6.57.1 - 2022-11-14 ------------------- This patch updates some internal type annotations and fixes a formatting bug in the :obj:`~hypothesis.Phase.explain` phase reporting. .. _v6.57.0: ------------------- 6.57.0 - 2022-11-14 ------------------- Hypothesis now raises an error if you passed a strategy as the ``alphabet=`` argument to :func:`~hypothesis.strategies.text`, and it generated something which was not a length-one string. This has never been supported, we're just adding explicit validation to catch cases like `this StackOverflow question `__. .. _v6.56.4: ------------------- 6.56.4 - 2022-10-28 ------------------- This patch updates some docs, and depends on :pypi:`exceptiongroup` 1.0.0 final to avoid a bug in the previous version. .. _v6.56.3: ------------------- 6.56.3 - 2022-10-17 ------------------- This patch teaches :func:`~hypothesis.strategies.text` to rewrite a few more filter predicates (:issue:`3134`). You're unlikely to notice any change. .. _v6.56.2: ------------------- 6.56.2 - 2022-10-10 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy, and fixes some incorrect examples in the docs for :func:`~hypothesis.extra.numpy.mutually_broadcastable_shapes`. .. _v6.56.1: ------------------- 6.56.1 - 2022-10-05 ------------------- This patch improves the error message when Hypothesis detects "flush to zero" mode for floating-point: we now report which package(s) enabled this, which can make debugging much easier. See :issue:`3458` for details. .. _v6.56.0: ------------------- 6.56.0 - 2022-10-02 ------------------- This release defines ``__bool__()`` on :class:`~hypothesis.strategies.SearchStrategy`. It always returns ``True``, like before, but also emits a warning to help with cases where you intended to draw a value (:issue:`3463`). .. _v6.55.0: ------------------- 6.55.0 - 2022-09-29 ------------------- In preparation for `future versions of the Array API standard `__, :func:`~hypothesis.extra.array_api.make_strategies_namespace` now accepts an optional ``api_version`` argument, which determines the version conformed to by the returned strategies namespace. If ``None``, the version of the passed array module ``xp`` is inferred. This release also introduces :func:`xps.real_dtypes`. This is currently equivalent to the existing :func:`xps.numeric_dtypes` strategy, but exists because the latter is expected to include complex numbers in the next version of the standard. .. _v6.54.6: ------------------- 6.54.6 - 2022-09-18 ------------------- If multiple explicit examples (from :obj:`@example() `) raise a Skip exception, for consistency with generated examples we now re-raise the first instead of collecting them into an ExceptionGroup (:issue:`3453`). .. _v6.54.5: ------------------- 6.54.5 - 2022-09-05 ------------------- This patch updates our autoformatting tools, improving our code style without any API changes. .. _v6.54.4: ------------------- 6.54.4 - 2022-08-20 ------------------- This patch fixes some type annotations for Python 3.9 and earlier (:issue:`3397`), and teaches the |Phase.explain| phase about certain locations it should not bother reporting (:issue:`3439`). .. _v6.54.3: ------------------- 6.54.3 - 2022-08-12 ------------------- This patch teaches the Ghostwriter an additional check for function and class locations that should make it use public APIs more often. .. _v6.54.2: ------------------- 6.54.2 - 2022-08-10 ------------------- This patch fixes our workaround for `a pytest bug where the inner exceptions in an ExceptionGroup are not displayed `__ (:issue:`3430`). .. _v6.54.1: ------------------- 6.54.1 - 2022-08-02 ------------------- This patch makes ``FailedHealthCheck`` and ``DeadlineExceeded`` exceptions picklable, for compatibility with Django's parallel test runner (:issue:`3426`). .. _v6.54.0: ------------------- 6.54.0 - 2022-08-02 ------------------- Reporting of :obj:`multiple failing examples ` now uses the :pep:`654` `ExceptionGroup `__ type, which is provided by the :pypi:`exceptiongroup` backport on Python 3.10 and earlier (:issue:`3175`). ``hypothesis.errors.MultipleFailures`` is therefore deprecated. Failing examples and other reports are now stored as :pep:`678` exception notes, which ensures that they will always appear together with the traceback and other information about their respective error. .. _v6.53.0: ------------------- 6.53.0 - 2022-07-25 ------------------- :func:`~hypothesis.extra.django.from_field` now supports ``UsernameField`` from :mod:`django.contrib.auth.forms`. Thanks to Afonso Silva for reporting and working on :issue:`3417`. .. _v6.52.4: ------------------- 6.52.4 - 2022-07-22 ------------------- This patch improves the error message when you pass filenames to the :command:`hypothesis write` CLI, which takes the name of a module or function (e.g. :command:`hypothesis write gzip` or :command:`hypothesis write package.some_function` rather than :command:`hypothesis write script.py`). Thanks to Ed Rogers for implementing this as part of the SciPy 2022 sprints! .. _v6.52.3: ------------------- 6.52.3 - 2022-07-19 ------------------- This patch ensures that the warning for non-interactive ``.example()`` points to your code instead of Hypothesis internals (:issue:`3403`). Thanks to @jameslamb for this fix. .. _v6.52.2: ------------------- 6.52.2 - 2022-07-19 ------------------- This patch makes :func:`~hypothesis.strategies.integers` more likely to generate boundary values for large two-sided intervals (:issue:`2942`). .. _v6.52.1: ------------------- 6.52.1 - 2022-07-18 ------------------- This patch adds filter rewriting for :func:`math.isfinite`, :func:`math.isinf`, and :func:`math.isnan` on :func:`~hypothesis.strategies.integers` or :func:`~hypothesis.strategies.floats` (:issue:`2701`). Thanks to Sam Clamons at the SciPy Sprints! .. _v6.52.0: ------------------- 6.52.0 - 2022-07-18 ------------------- This release adds the ``allow_subnormal`` argument to :func:`~hypothesis.strategies.complex_numbers` by applying it to each of the real and imaginary parts separately. Closes :issue:`3390`. Thanks to Evan Tey for this fix. .. _v6.51.0: ------------------- 6.51.0 - 2022-07-17 ------------------- Issue a deprecation warning if a function decorated with :func:`@composite ` does not draw any values (:issue:`3384`). Thanks to Grzegorz Zieba, Rodrigo Girão, and Thomas Ball for working on this at the EuroPython sprints! .. _v6.50.1: ------------------- 6.50.1 - 2022-07-09 ------------------- This patch improves the error messages in :obj:`@example() ` argument validation following the recent release of :ref:`6.49.1 `. .. _v6.50.0: ------------------- 6.50.0 - 2022-07-09 ------------------- This release allows :func:`~hypothesis.extra.numpy.from_dtype` to generate Unicode strings which cannot be encoded in UTF-8, but are valid in Numpy arrays (which use UTF-32). This logic will only be used with :pypi:`numpy` >= 1.19, because earlier versions have `an issue `__ which led us to revert :ref:`Hypothesis 5.2 ` last time! .. _v6.49.1: ------------------- 6.49.1 - 2022-07-05 ------------------- This patch fixes some inconsistency between argument handling for :obj:`@example ` and :func:`@given ` (:issue:`2706 <2706#issuecomment-1168363177>`). .. _v6.49.0: ------------------- 6.49.0 - 2022-07-04 ------------------- This release uses :pep:`612` :obj:`python:typing.ParamSpec` (or the :pypi:`typing-extensions` backport) to express the first-argument-removing behaviour of :func:`@st.composite ` and signature-preservation of :func:`~hypothesis.strategies.functions` to IDEs, editor plugins, and static type checkers such as :pypi:`mypy`. .. _v6.48.3: ------------------- 6.48.3 - 2022-07-03 ------------------- :func:`hypothesis.event` now works for hashable objects which do not support weakrefs, such as integers and tuples. .. _v6.48.2: ------------------- 6.48.2 - 2022-06-29 ------------------- This patch tidies up some internal introspection logic, which will improve support for positional-only arguments in a future release (:issue:`2706`). .. _v6.48.1: ------------------- 6.48.1 - 2022-06-27 ------------------- This release automatically rewrites some simple filters, such as ``floats().filter(lambda x: x >= 10)`` to the more efficient ``floats(min_value=10)``, based on the AST of the predicate. We continue to recommend using the efficient form directly wherever possible, but this should be useful for e.g. :pypi:`pandera` "``Checks``" where you already have a simple predicate and translating manually is really annoying. See :issue:`2701` for details. .. _v6.48.0: ------------------- 6.48.0 - 2022-06-27 ------------------- This release raises :class:`~unittest.SkipTest` for tests which never executed any examples, for example because the :obj:`~hypothesis.settings.phases` setting excluded the :obj:`~hypothesis.Phase.explicit`, :obj:`~hypothesis.Phase.reuse`, and :obj:`~hypothesis.Phase.generate` phases. This helps to avoid cases where broken tests appear to pass, because they didn't actually execute (:issue:`3328`). .. _v6.47.5: ------------------- 6.47.5 - 2022-06-25 ------------------- This patch fixes type annotations that had caused the signature of :func:`@given ` to be partially-unknown to type-checkers for Python versions before 3.10. .. _v6.47.4: ------------------- 6.47.4 - 2022-06-23 ------------------- This patch fixes :func:`~hypothesis.strategies.from_type` on Python 3.11, following `python/cpython#93754 `__. .. _v6.47.3: ------------------- 6.47.3 - 2022-06-15 ------------------- This patch makes the :obj:`~hypothesis.HealthCheck.too_slow` health check more consistent with long :obj:`~hypothesis.settings.deadline` tests (:issue:`3367`) and fixes an install issue under :pypi:`pipenv` which was introduced in :ref:`Hypothesis 6.47.2 ` (:issue:`3374`). .. _v6.47.2: ------------------- 6.47.2 - 2022-06-12 ------------------- We now use the :pep:`654` `ExceptionGroup `__ type - provided by the :pypi:`exceptiongroup` backport on older Pythons - to ensure that if multiple errors are raised in teardown, they will all propagate. .. _v6.47.1: ------------------- 6.47.1 - 2022-06-10 ------------------- Our pretty-printer no longer sorts dictionary keys, since iteration order is stable in Python 3.7+ and this can affect reproducing examples (:issue:`3370`). This PR was kindly supported by `Ordina Pythoneers `__. .. _v6.47.0: ------------------- 6.47.0 - 2022-06-07 ------------------- The :ref:`Ghostwriter ` can now write tests for :obj:`@classmethod ` or :obj:`@staticmethod ` methods, in addition to the existing support for functions and other callables (:issue:`3318`). Thanks to Cheuk Ting Ho for the patch. .. _v6.46.11: -------------------- 6.46.11 - 2022-06-02 -------------------- Mention :func:`hypothesis.strategies.timezones` in the documentation of :func:`hypothesis.strategies.datetimes` for completeness. Thanks to George Macon for this addition. .. _v6.46.10: -------------------- 6.46.10 - 2022-06-01 -------------------- This release contains some small improvements to our documentation. Thanks to Felix Divo for his contribution! .. _v6.46.9: ------------------- 6.46.9 - 2022-05-25 ------------------- This patch by Adrian Garcia Badaracco adds type annotations to some private internals (:issue:`3074`). .. _v6.46.8: ------------------- 6.46.8 - 2022-05-25 ------------------- This patch by Phillip Schanely makes changes to the :func:`~hypothesis.strategies.floats` strategy when ``min_value`` or ``max_value`` is present. Hypothesis will now be capable of generating every representable value in the bounds. You may notice that hypothesis is more likely to test values near boundaries, and values that are very close to zero. These changes also support future integrations with symbolic execution tools and fuzzers (:issue:`3086`). .. _v6.46.7: ------------------- 6.46.7 - 2022-05-19 ------------------- This patch updates the type annotations for :func:`~hypothesis.strategies.tuples` and :func:`~hypothesis.strategies.one_of` so that type-checkers require its arguments to be positional-only, and so that it no longer fails under pyright-strict mode (see :issue:`3348`). Additional changes are made to Hypothesis' internals improve pyright scans. .. _v6.46.6: ------------------- 6.46.6 - 2022-05-18 ------------------- This patch by Cheuk Ting Ho adds support for :pep:`655` ``Required`` and ``NotRequired`` as attributes of :obj:`~python:typing.TypedDict` in :func:`~hypothesis.strategies.from_type` (:issue:`3339`). .. _v6.46.5: ------------------- 6.46.5 - 2022-05-15 ------------------- This patch fixes :func:`~hypothesis.extra.numpy.from_dtype` with long-precision floating-point datatypes (typecode ``g``; see :func:`numpy:numpy.typename`). .. _v6.46.4: ------------------- 6.46.4 - 2022-05-15 ------------------- This patch improves some error messages for custom signatures containing invalid parameter names (:issue:`3317`). .. _v6.46.3: ------------------- 6.46.3 - 2022-05-11 ------------------- This patch by Cheuk Ting Ho makes it an explicit error to call :func:`~hypothesis.strategies.from_type` or :func:`~hypothesis.strategies.register_type_strategy` with types that have no runtime instances (:issue:`3280`). .. _v6.46.2: ------------------- 6.46.2 - 2022-05-03 ------------------- This patch fixes silently dropping examples when the :obj:`@example ` decorator is applied to itself (:issue:`3319`). This was always a weird pattern, but now it works. Thanks to Ray Sogata, Keeri Tramm, and Kevin Khuong for working on this patch! .. _v6.46.1: ------------------- 6.46.1 - 2022-05-01 ------------------- This patch fixes a rare bug where we could incorrectly treat :obj:`~python:inspect.Parameter.empty` as a type annotation, if the callable had an explicitly assigned ``__signature__``. .. _v6.46.0: ------------------- 6.46.0 - 2022-05-01 ------------------- This release adds an ``allow_nil`` argument to :func:`~hypothesis.strategies.uuids`, which you can use to... generate the nil UUID. Thanks to Shlok Gandhi for the patch! .. _v6.45.4: ------------------- 6.45.4 - 2022-05-01 ------------------- This patch fixes some missing imports for certain :ref:`Ghostwritten ` tests. Thanks to Mel Seto for fixing :issue:`3316`. .. _v6.45.3: ------------------- 6.45.3 - 2022-04-30 ------------------- This patch teaches :ref:`the Ghostwriter ` to recognize many more common argument names (:issue:`3311`). .. _v6.45.2: ------------------- 6.45.2 - 2022-04-29 ------------------- This patch fixes :issue:`3314`, where Hypothesis would raise an internal error from :func:`~hypothesis.provisional.domains` or (only on Windows) from :func:`~hypothesis.strategies.timezones` in some rare circumstances where the installation was subtly broken. Thanks to Munir Abdinur for this contribution. .. _v6.45.1: ------------------- 6.45.1 - 2022-04-27 ------------------- This release fixes deprecation warnings about ``sre_compile`` and ``sre_parse`` imports and ``importlib.resources`` usage when running Hypothesis on Python 3.11. Thanks to Florian Bruhin for this contribution. .. _v6.45.0: ------------------- 6.45.0 - 2022-04-22 ------------------- This release updates :func:`xps.indices` by introducing an ``allow_newaxis`` argument, defaulting to ``False``. If ``allow_newaxis=True``, indices can be generated that add dimensions to arrays, which is achieved by the indexer containing ``None``. This change is to support a specification change that expand dimensions via indexing (`data-apis/array-api#408 `_). .. _v6.44.0: ------------------- 6.44.0 - 2022-04-21 ------------------- This release adds a ``names`` argument to :func:`~hypothesis.extra.pandas.indexes` and :func:`~hypothesis.extra.pandas.series`, so that you can create Pandas objects with specific or varied names. Contributed by Sam Watts. .. _v6.43.3: ------------------- 6.43.3 - 2022-04-18 ------------------- This patch updates the type annotations for :func:`@given ` so that type-checkers will warn on mixed positional and keyword arguments, as well as fixing :issue:`3296`. .. _v6.43.2: ------------------- 6.43.2 - 2022-04-16 ------------------- Fixed a type annotation for ``pyright --strict`` (:issue:`3287`). .. _v6.43.1: ------------------- 6.43.1 - 2022-04-13 ------------------- This patch makes it an explicit error to call :func:`~hypothesis.strategies.register_type_strategy` with a `Pydantic GenericModel `__ and a callable, because ``GenericModel`` isn't actually a generic type at runtime and so you have to register each of the "parametrized versions" (actually subclasses!) manually. See :issue:`2940` for more details. .. _v6.43.0: ------------------- 6.43.0 - 2022-04-12 ------------------- This release makes it an explicit error to apply :func:`@pytest.fixture ` to a function which has already been decorated with :func:`@given() `. Previously, ``pytest`` would convert your test to a fixture, and then never run it. .. _v6.42.3: ------------------- 6.42.3 - 2022-04-10 ------------------- This patch fixes :func:`~hypothesis.strategies.from_type` on a :obj:`~python:typing.TypedDict` with complex annotations, defined in a file using ``from __future__ import annotations``. Thanks to Katelyn Gigante for identifying and fixing this bug! .. _v6.42.2: ------------------- 6.42.2 - 2022-04-10 ------------------- The Hypothesis pytest plugin was not outputting valid xunit2 nodes when ``--junit-xml`` was specified. This has been broken since Pytest 5.4, which changed the internal API for adding nodes to the junit report. This also fixes the issue when using hypothesis with ``--junit-xml`` and ``pytest-xdist`` where the junit xml report would not be xunit2 compatible. Now, when using with ``pytest-xdist``, the junit report will just omit the ```` node. For more details, see `this pytest issue `__, `this pytest issue `__, and :issue:`1935`. Thanks to Brandon Chinn for this bug fix! .. _v6.42.1: ------------------- 6.42.1 - 2022-04-10 ------------------- This patch fixes pretty-printing of regular expressions in Python 3.11.0a7, and updates our vendored `list of top-level domains `__,. .. _v6.42.0: ------------------- 6.42.0 - 2022-04-09 ------------------- This release makes :func:`st.functions(pure=True) ` less noisy (:issue:`3253`), and generally improves pretty-printing of functions. .. _v6.41.0: ------------------- 6.41.0 - 2022-04-01 ------------------- This release changes the implementation of :const:`~hypothesis.infer` to be an alias for :obj:`python:Ellipsis`. E.g. ``@given(a=infer)`` is now equivalent to ``@given(a=...)``. Furthermore, ``@given(...)`` can now be specified so that :func:`@given ` will infer the strategies for *all* arguments of the decorated function based on its annotations. .. _v6.40.3: ------------------- 6.40.3 - 2022-04-01 ------------------- This patch simplifies the repr of the strategies namespace returned in :func:`~hypothesis.extra.array_api.make_strategies_namespace`, e.g. .. code-block:: pycon >>> from hypothesis.extra.array_api import make_strategies_namespace >>> from numpy import array_api as xp >>> xps = make_strategies_namespace(xp) >>> xps make_strategies_namespace(numpy.array_api) .. _v6.40.2: ------------------- 6.40.2 - 2022-04-01 ------------------- Fixed :func:`~hypothesis.strategies.from_type` support for :pep:`604` union types, like ``int | None`` (:issue:`3255`). .. _v6.40.1: ------------------- 6.40.1 - 2022-04-01 ------------------- Fixed an internal error when :func:`~hypothesis.given` was passed a lambda. .. _v6.40.0: ------------------- 6.40.0 - 2022-03-29 ------------------- :ref:`The Ghostwriter ` can now write tests which check that two or more functions are equivalent on valid inputs, *or* raise the same type of exception for invalid inputs (:issue:`3267`). .. _v6.39.6: ------------------- 6.39.6 - 2022-03-27 ------------------- This patch makes some quality-of-life improvements to the :ref:`Ghostwriter `: we guess the :func:`~hypothesis.strategies.text` strategy for arguments named ``text`` (...obvious in hindsight, eh?); and improved the error message if you accidentally left in a :func:`~hypothesis.strategies.nothing` or broke your :pypi:`rich` install. .. _v6.39.5: ------------------- 6.39.5 - 2022-03-26 ------------------- This patch improves our error detection and message when Hypothesis is run on a Python implementation without support for ``-0.0``, which is required for the :func:`~hypothesis.strategies.floats` strategy but can be disabled by `unsafe compiler options `__ (:issue:`3265`). .. _v6.39.4: ------------------- 6.39.4 - 2022-03-17 ------------------- This patch tweaks some internal formatting. There is no user-visible change. .. _v6.39.3: ------------------- 6.39.3 - 2022-03-07 ------------------- If the :obj:`~hypothesis.Phase.shrink` phase is disabled, we now stop the :obj:`~hypothesis.Phase.generate` phase as soon as an error is found regardless of the value of the ``report_multiple_examples`` setting, since that's probably what you wanted (:issue:`3244`). .. _v6.39.2: ------------------- 6.39.2 - 2022-03-07 ------------------- This patch clarifies rare error messages in :func:`~hypothesis.strategies.builds` (:issue:`3225`) and :func:`~hypothesis.strategies.floats` (:issue:`3207`). .. _v6.39.1: ------------------- 6.39.1 - 2022-03-03 ------------------- This patch fixes a regression where the bound inner function (``your_test.hypothesis.inner_test``) would be invoked with positional arguments rather than passing them by name, which broke :pypi:`pytest-asyncio` (:issue:`3245`). .. _v6.39.0: ------------------- 6.39.0 - 2022-03-01 ------------------- This release improves Hypothesis' handling of positional-only arguments, which are now allowed :func:`@st.composite ` strategies. On Python 3.8 and later, the first arguments to :func:`~hypothesis.strategies.builds` and :func:`~hypothesis.extra.django.from_model` are now natively positional-only. In cases which were already errors, the ``TypeError`` from incorrect usage will therefore be raises immediately when the function is called, rather than when the strategy object is used. .. _v6.38.0: ------------------- 6.38.0 - 2022-02-26 ------------------- This release makes :func:`~hypothesis.strategies.floats` error *consistently* when your floating-point hardware has been configured to violate IEEE-754 for :wikipedia:`subnormal numbers `, instead of only when an internal assertion was tripped (:issue:`3092`). If this happens to you, passing ``allow_subnormal=False`` will suppress the explicit error. However, we strongly recommend fixing the root cause by disabling global-effect unsafe-math compiler options instead, or at least consulting e.g. Simon Byrne's `Beware of fast-math `__ explainer first. .. _v6.37.2: ------------------- 6.37.2 - 2022-02-21 ------------------- This patch fixes a bug in stateful testing, where returning a single value wrapped in :func:`~hypothesis.stateful.multiple` would be printed such that the assigned variable was a tuple rather than the single element (:issue:`3236`). .. _v6.37.1: ------------------- 6.37.1 - 2022-02-21 ------------------- This patch fixes a warning under :pypi:`pytest` 7 relating to our rich traceback display logic (:issue:`3223`). .. _v6.37.0: ------------------- 6.37.0 - 2022-02-18 ------------------- When distinguishing multiple errors, Hypothesis now looks at the inner exceptions of :pep:`654` ``ExceptionGroup``\ s. .. _v6.36.2: ------------------- 6.36.2 - 2022-02-13 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.36.1: ------------------- 6.36.1 - 2022-01-31 ------------------- This patch fixes some deprecation warnings from :pypi:`pytest` 7.0, along with some code formatting and docs updates. .. _v6.36.0: ------------------- 6.36.0 - 2022-01-19 ------------------- This release disallows using :obj:`python:typing.Final` with :func:`~hypothesis.strategies.from_type` and :func:`~hypothesis.strategies.register_type_strategy`. Why? Because ``Final`` can only be used during ``class`` definition. We don't generate class attributes. It also does not make sense as a runtime type on its own. .. _v6.35.1: ------------------- 6.35.1 - 2022-01-17 ------------------- This patch fixes ``hypothesis write`` output highlighting with :pypi:`rich` version 12.0 and later. .. _v6.35.0: ------------------- 6.35.0 - 2022-01-08 ------------------- This release disallows using :obj:`python:typing.ClassVar` with :func:`~hypothesis.strategies.from_type` and :func:`~hypothesis.strategies.register_type_strategy`. Why? Because ``ClassVar`` can only be used during ``class`` definition. We don't generate class attributes. It also does not make sense as a runtime type on its own. .. _v6.34.2: ------------------- 6.34.2 - 2022-01-05 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.34.1: ------------------- 6.34.1 - 2021-12-31 ------------------- This patch fixes :issue:`3169`, an extremely rare bug which would trigger if an internal least-recently-reused cache dropped a newly added entry immediately after it was added. .. _v6.34.0: ------------------- 6.34.0 - 2021-12-31 ------------------- This release fixes :issue:`3133` and :issue:`3144`, where attempting to generate Pandas series of lists or sets would fail with confusing errors if you did not specify ``dtype=object``. .. _v6.33.0: ------------------- 6.33.0 - 2021-12-30 ------------------- This release disallows using :obj:`python:typing.TypeAlias` with :func:`~hypothesis.strategies.from_type` and :func:`~hypothesis.strategies.register_type_strategy`. Why? Because ``TypeAlias`` is not really a type, it is a tag for type checkers that some expression is a type alias, not something else. It does not make sense for Hypothesis to resolve it as a strategy. References :issue:`2978`. .. _v6.32.1: ------------------- 6.32.1 - 2021-12-23 ------------------- This patch updates our autoformatting tools, improving our code style without any API changes. .. _v6.32.0: ------------------- 6.32.0 - 2021-12-23 ------------------- This release drops support for Python 3.6, which `reached end of life upstream `__ on 2021-12-23. .. _v6.31.6: ------------------- 6.31.6 - 2021-12-15 ------------------- This patch adds a temporary hook for a downstream tool, which is not part of the public API. .. _v6.31.5: ------------------- 6.31.5 - 2021-12-14 ------------------- This release updates our copyright headers to `use a general authorship statement and omit the year `__. .. _v6.31.4: ------------------- 6.31.4 - 2021-12-11 ------------------- This patch makes the ``.example()`` method more representative of test-time data generation, albeit often at a substantial cost to readability (:issue:`3182`). .. _v6.31.3: ------------------- 6.31.3 - 2021-12-10 ------------------- This patch improves annotations on some of Hypothesis' internal functions, in order to deobfuscate the signatures of some strategies. In particular, strategies shared between :ref:`hypothesis.extra.numpy ` and :ref:`the hypothesis.extra.array_api extra ` will benefit from this patch. .. _v6.31.2: ------------------- 6.31.2 - 2021-12-10 ------------------- This patch fix invariants display in stateful falsifying examples (:issue:`3185`). .. _v6.31.1: ------------------- 6.31.1 - 2021-12-10 ------------------- This patch updates :func:`xps.indices` so no flat indices are generated, i.e. generated indices will now always explicitly cover each axes of an array if no ellipsis is present. This is to be consistent with a specification change that dropped support for flat indexing (`#272 `_). .. _v6.31.0: ------------------- 6.31.0 - 2021-12-09 ------------------- This release makes us compatible with :pypi:`Django` 4.0, in particular by adding support for use of :mod:`zoneinfo` timezones (though we respect the new ``USE_DEPRECATED_PYTZ`` setting if you need it). .. _v6.30.1: ------------------- 6.30.1 - 2021-12-05 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.30.0: ------------------- 6.30.0 - 2021-12-03 ------------------- This release adds an ``allow_subnormal`` argument to the :func:`~hypothesis.strategies.floats` strategy, which can explicitly toggle the generation of :wikipedia:`subnormal floats ` (:issue:`3155`). Disabling such generation is useful when testing flush-to-zero builds of libraries. :func:`nps.from_dtype() ` and :func:`xps.from_dtype` can also accept the ``allow_subnormal`` argument, and :func:`xps.from_dtype` or :func:`xps.arrays` will disable subnormals by default if the array module ``xp`` is detected to flush-to-zero (like is typical with CuPy). .. _v6.29.3: ------------------- 6.29.3 - 2021-12-02 ------------------- This patch fixes a bug in :func:`~hypothesis.extra.numpy.mutually_broadcastable_shapes`, which restricted the patterns of singleton dimensions that could be generated for dimensions that extended beyond ``base_shape`` (:issue:`3170`). .. _v6.29.2: ------------------- 6.29.2 - 2021-12-02 ------------------- This patch clarifies our pretty-printing of DataFrames (:issue:`3114`). .. _v6.29.1: ------------------- 6.29.1 - 2021-12-02 ------------------- This patch documents :func:`~hypothesis.strategies.timezones` `Windows-only requirement `__ for the :pypi:`tzdata` package, and ensures that ``pip install hypothesis[zoneinfo]`` will install the latest version. .. _v6.29.0: ------------------- 6.29.0 - 2021-11-29 ------------------- This release teaches :func:`~hypothesis.strategies.builds` to use :func:`~hypothesis.strategies.deferred` when resolving unrecognised type hints, so that you can conveniently register strategies for recursive types with constraints on some arguments (:issue:`3026`): .. code-block:: python class RecursiveClass: def __init__(self, value: int, next_node: typing.Optional["SomeClass"]): assert value > 0 self.value = value self.next_node = next_node st.register_type_strategy( RecursiveClass, st.builds(RecursiveClass, value=st.integers(min_value=1)) ) .. _v6.28.1: ------------------- 6.28.1 - 2021-11-28 ------------------- This release fixes some internal calculations related to collection sizes (:issue:`3143`). .. _v6.28.0: ------------------- 6.28.0 - 2021-11-28 ------------------- This release modifies our :pypi:`pytest` plugin, to avoid importing Hypothesis and therefore triggering :ref:`Hypothesis' entry points ` for test suites where Hypothesis is installed but not actually used (:issue:`3140`). .. _v6.27.3: ------------------- 6.27.3 - 2021-11-28 ------------------- This release fixes :issue:`3080`, where :func:`~hypothesis.strategies.from_type` failed on unions containing :pep:`585` builtin generic types (like ``list[int]``) in Python 3.9 and later. .. _v6.27.2: ------------------- 6.27.2 - 2021-11-26 ------------------- This patch makes the :command:`hypothesis codemod` :ref:`command ` somewhat faster. .. _v6.27.1: ------------------- 6.27.1 - 2021-11-22 ------------------- This patch changes the backing datastructures of :func:`~hypothesis.register_random` and a few internal caches to use :class:`weakref.WeakValueDictionary`. This reduces memory usage and may improve performance when registered :class:`~random.Random` instances are only used for a subset of your tests (:issue:`3131`). .. _v6.27.0: ------------------- 6.27.0 - 2021-11-22 ------------------- This release teaches Hypothesis' multiple-error reporting to format tracebacks using :pypi:`pytest` or :pypi:`better-exceptions`, if they are installed and enabled (:issue:`3116`). .. _v6.26.0: ------------------- 6.26.0 - 2021-11-21 ------------------- Did you know that of the 2\ :superscript:`64` possible floating-point numbers, 2\ :superscript:`53` of them are ``nan`` - and Python prints them all the same way? While nans *usually* have all zeros in the sign bit and mantissa, this `isn't always true `__, and :wikipedia:`'signaling' nans might trap or error `. To help distinguish such errors in e.g. CI logs, Hypothesis now prints ``-nan`` for negative nans, and adds a comment like ``# Saw 3 signaling NaNs`` if applicable. .. _v6.25.0: ------------------- 6.25.0 - 2021-11-19 ------------------- This release adds special filtering logic to make a few special cases like ``s.map(lambda x: x)`` and ``lists().filter(len)`` more efficient (:issue:`2701`). .. _v6.24.6: ------------------- 6.24.6 - 2021-11-18 ------------------- This patch makes :func:`~hypothesis.strategies.floats` generate :wikipedia:`"subnormal" floating point numbers ` more often, as these rare values can have strange interactions with `unsafe compiler optimisations like -ffast-math `__ (:issue:`2976`). .. _v6.24.5: ------------------- 6.24.5 - 2021-11-16 ------------------- This patch fixes a rare internal error in the :func:`~hypothesis.strategies.datetimes` strategy, where the implementation of ``allow_imaginary=False`` crashed when checking a time during the skipped hour of a DST transition *if* the DST offset is negative - only true of ``Europe/Dublin``, who we presume have their reasons - and the ``tzinfo`` object is a :pypi:`pytz` timezone (which predates :pep:`495`). .. _v6.24.4: ------------------- 6.24.4 - 2021-11-15 ------------------- This patch gives Hypothesis it's own internal :class:`~random.Random` instance, ensuring that test suites which reset the global random state don't induce weird correlations between property-based tests (:issue:`2135`). .. _v6.24.3: ------------------- 6.24.3 - 2021-11-13 ------------------- This patch updates documentation of :func:`~hypothesis.note` (:issue:`3147`). .. _v6.24.2: ------------------- 6.24.2 - 2021-11-05 ------------------- This patch updates internal testing for the :ref:`Array API extra ` to be consistent with new specification changes: ``sum()`` not accepting boolean arrays (`#234 `_), ``unique()`` split into separate functions (`#275 `_), and treating NaNs as distinct (`#310 `_). It has no user visible impact. .. _v6.24.1: ------------------- 6.24.1 - 2021-11-01 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. .. _v6.24.0: ------------------- 6.24.0 - 2021-10-23 ------------------- This patch updates our vendored `list of top-level domains `__, which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. (did you know that gTLDs can be both `added `__ and `removed `__?) .. _v6.23.4: ------------------- 6.23.4 - 2021-10-20 ------------------- This patch adds an error for when ``shapes`` in :func:`xps.arrays()` is not passed as either a valid shape or strategy. .. _v6.23.3: ------------------- 6.23.3 - 2021-10-18 ------------------- This patch updates our formatting with :pypi:`shed`. .. _v6.23.2: ------------------- 6.23.2 - 2021-10-08 ------------------- This patch replaces external links to :doc:`NumPy ` API docs with :mod:`sphinx.ext.intersphinx` cross-references. It is purely a documentation improvement. .. _v6.23.1: ------------------- 6.23.1 - 2021-09-29 ------------------- This patch cleans up internal logic for :func:`xps.arrays()`. There is no user-visible change. .. _v6.23.0: ------------------- 6.23.0 - 2021-09-26 ------------------- This release follows :pypi:`pytest` in considering :class:`SystemExit` and :class:`GeneratorExit` exceptions to be test failures, meaning that we will shink to minimal examples and check for flakiness even though they subclass :class:`BaseException` directly (:issue:`2223`). :class:`KeyboardInterrupt` continues to interrupt everything, and will be re-raised immediately. .. _v6.22.0: ------------------- 6.22.0 - 2021-09-24 ------------------- This release adds :class:`~hypothesis.extra.django.LiveServerTestCase` and :class:`~hypothesis.extra.django.StaticLiveServerTestCase` for django test. Thanks to Ivan Tham for this feature! .. _v6.21.6: ------------------- 6.21.6 - 2021-09-19 ------------------- This patch fixes some new linter warnings such as :pypi:`flake8-bugbear`'s ``B904`` for explicit exception chaining, so tracebacks might be a bit nicer. .. _v6.21.5: ------------------- 6.21.5 - 2021-09-16 ------------------- This release fixes ``None`` being inferred as the float64 dtype in :func:`~xps.from_dtype()` and :func:`~xps.arrays()` from the :ref:`Array API extra `. .. _v6.21.4: ------------------- 6.21.4 - 2021-09-16 ------------------- This release fixes the type hint for the :func:`@given() ` decorator when decorating an ``async`` function (:issue:`3099`). .. _v6.21.3: ------------------- 6.21.3 - 2021-09-15 ------------------- This release improves Ghostwritten tests for builtins (:issue:`2977`). .. _v6.21.2: ------------------- 6.21.2 - 2021-09-15 ------------------- This release deprecates use of both ``min_dims > len(shape)`` and ``max_dims > len(shape)`` when ``allow_newaxis == False`` in :func:`~hypothesis.extra.numpy.basic_indices` (:issue:`3091`). .. _v6.21.1: ------------------- 6.21.1 - 2021-09-13 ------------------- This release improves the behaviour of :func:`~hypothesis.strategies.builds` and :func:`~hypothesis.strategies.from_type` in certain situations involving decorators (:issue:`2495` and :issue:`3029`). .. _v6.21.0: ------------------- 6.21.0 - 2021-09-11 ------------------- This release introduces strategies for array/tensor libraries adopting the `Array API standard `__ (:issue:`3037`). They are available in :ref:`the hypothesis.extra.array_api extra `, and work much like the existing :ref:`strategies for NumPy `. .. _v6.20.1: ------------------- 6.20.1 - 2021-09-10 ------------------- This patch fixes :issue:`961`, where calling ``given()`` inline on a bound method would fail to handle the ``self`` argument correctly. .. _v6.20.0: ------------------- 6.20.0 - 2021-09-09 ------------------- This release allows :func:`~hypothesis.strategies.slices` to generate ``step=None``, and fixes an off-by-one error where the ``start`` index could be equal to ``size``. This works fine for all Python sequences and Numpy arrays, but is undefined behaviour in the `Array API standard `__ (see :pull:`3065`). .. _v6.19.0: ------------------- 6.19.0 - 2021-09-08 ------------------- This release makes :ref:`stateful testing ` more likely to tell you if you do something unexpected and unsupported: - The :obj:`~hypothesis.HealthCheck.return_value` health check now applies to :func:`~hypothesis.stateful.rule` and :func:`~hypothesis.stateful.initialize` rules, if they don't have ``target`` bundles, as well as :func:`~hypothesis.stateful.invariant`. - Using a :func:`~hypothesis.stateful.consumes` bundle as a ``target`` is deprecated, and will be an error in a future version. If existing code triggers these new checks, check for related bugs and misunderstandings - these patterns *never* had any effect. .. _v6.18.0: ------------------- 6.18.0 - 2021-09-06 ------------------- This release teaches :func:`~hypothesis.strategies.from_type` a neat trick: when resolving an :obj:`python:typing.Annotated` type, if one of the annotations is a strategy object we use that as the inferred strategy. For example: .. code-block:: python PositiveInt = Annotated[int, st.integers(min_value=1)] If there are multiple strategies, we use the last outer-most annotation. See :issue:`2978` and :pull:`3082` for discussion. *Requires Python 3.9 or later for* :func:`get_type_hints(..., include_extras=False) `. .. _v6.17.4: ------------------- 6.17.4 - 2021-08-31 ------------------- This patch makes unique :func:`~hypothesis.extra.numpy.arrays` much more efficient, especially when there are only a few valid elements - such as for eight-bit integers (:issue:`3066`). .. _v6.17.3: ------------------- 6.17.3 - 2021-08-30 ------------------- This patch fixes the repr of :func:`~hypothesis.extra.numpy.array_shapes`. .. _v6.17.2: ------------------- 6.17.2 - 2021-08-30 ------------------- This patch wraps some internal helper code in our proxies decorator to prevent mutations of method docstrings carrying over to other instances of the respective methods. .. _v6.17.1: ------------------- 6.17.1 - 2021-08-29 ------------------- This patch moves some internal helper code in preparation for :issue:`3065`. There is no user-visible change, unless you depended on undocumented internals. .. _v6.17.0: ------------------- 6.17.0 - 2021-08-27 ------------------- This release adds type annotations to the :ref:`stateful testing ` API. Thanks to Ruben Opdebeeck for this contribution! .. _v6.16.0: ------------------- 6.16.0 - 2021-08-27 ------------------- This release adds the :class:`~hypothesis.strategies.DrawFn` type as a reusable type hint for the ``draw`` argument of :func:`@composite ` functions. Thanks to Ruben Opdebeeck for this contribution! .. _v6.15.0: ------------------- 6.15.0 - 2021-08-22 ------------------- This release emits a more useful error message when :func:`@given() ` is applied to a coroutine function, i.e. one defined using ``async def`` (:issue:`3054`). This was previously only handled by the generic :obj:`~hypothesis.HealthCheck.return_value` health check, which doesn't direct you to use either :ref:`a custom executor ` or a library such as :pypi:`pytest-trio` or :pypi:`pytest-asyncio` to handle it for you. .. _v6.14.9: ------------------- 6.14.9 - 2021-08-20 ------------------- This patch fixes a regression in Hypothesis 6.14.8, where :func:`~hypothesis.strategies.from_type` failed to resolve types which inherit from multiple parametrised generic types, affecting the :pypi:`returns` package (:issue:`3060`). .. _v6.14.8: ------------------- 6.14.8 - 2021-08-16 ------------------- This patch ensures that registering a strategy for a subclass of a parametrised generic type such as ``class Lines(Sequence[str]):`` will not "leak" into unrelated strategies such as ``st.from_type(Sequence[int])`` (:issue:`2951`). Unfortunately this fix requires :pep:`560`, meaning Python 3.7 or later. .. _v6.14.7: ------------------- 6.14.7 - 2021-08-14 ------------------- This patch fixes :issue:`3050`, where :pypi:`attrs` classes could cause an internal error in the :ref:`ghostwriter `. .. _v6.14.6: ------------------- 6.14.6 - 2021-08-07 ------------------- This patch improves the error message for :issue:`3016`, where :pep:`585` builtin generics with self-referential forward-reference strings cannot be resolved to a strategy by :func:`~hypothesis.strategies.from_type`. .. _v6.14.5: ------------------- 6.14.5 - 2021-07-27 ------------------- This patch fixes ``hypothesis.strategies._internal.types.is_a_new_type``. It was failing on Python ``3.10.0b4``, where ``NewType`` is a function. .. _v6.14.4: ------------------- 6.14.4 - 2021-07-26 ------------------- This patch fixes :func:`~hypothesis.strategies.from_type` and :func:`~hypothesis.strategies.register_type_strategy` for :obj:`python:typing.NewType` on Python 3.10, which changed the underlying implementation (see :bpo:`44353` for details). .. _v6.14.3: ------------------- 6.14.3 - 2021-07-18 ------------------- This patch updates our autoformatting tools, improving our code style without any API changes. .. _v6.14.2: ------------------- 6.14.2 - 2021-07-12 ------------------- This patch ensures that we shorten tracebacks for tests which fail due to inconsistent data generation between runs (i.e. raise ``Flaky``). .. _v6.14.1: ------------------- 6.14.1 - 2021-07-02 ------------------- This patch updates some internal type annotations. There is no user-visible change. .. _v6.14.0: ------------------- 6.14.0 - 2021-06-09 ------------------- The |Phase.explain| phase now requires shrinking to be enabled, and will be automatically skipped for deadline-exceeded errors. .. _v6.13.14: -------------------- 6.13.14 - 2021-06-04 -------------------- This patch improves the :func:`~hypothesis.strategies.tuples` strategy type annotations, to preserve the element types for up to length-five tuples (:issue:`3005`). As for :func:`~hypothesis.strategies.one_of`, this is the best we can do before a `planned extension `__ to :pep:`646` is released, hopefully in Python 3.11. .. _v6.13.13: -------------------- 6.13.13 - 2021-06-04 -------------------- This patch teaches :ref:`the Ghostwriter ` how to find :doc:`custom ufuncs ` from *any* module that defines them, and that ``yaml.unsafe_load()`` does not undo ``yaml.safe_load()``. .. _v6.13.12: -------------------- 6.13.12 - 2021-06-03 -------------------- This patch reduces the amount of internal code excluded from our test suite's code coverage checks. There is no user-visible change. .. _v6.13.11: -------------------- 6.13.11 - 2021-06-02 -------------------- This patch removes some old internal helper code that previously existed to make Python 2 compatibility easier. There is no user-visible change. .. _v6.13.10: -------------------- 6.13.10 - 2021-05-30 -------------------- This release adjusts some internal code to help make our test suite more reliable. There is no user-visible change. .. _v6.13.9: ------------------- 6.13.9 - 2021-05-30 ------------------- This patch cleans up some internal code related to filtering strategies. There is no user-visible change. .. _v6.13.8: ------------------- 6.13.8 - 2021-05-28 ------------------- This patch slightly improves the performance of some internal code for generating integers. .. _v6.13.7: ------------------- 6.13.7 - 2021-05-27 ------------------- This patch fixes a bug in :func:`~hypothesis.strategies.from_regex` that caused ``from_regex("", fullmatch=True)`` to unintentionally generate non-empty strings (:issue:`4982`). The only strings that completely match an empty regex pattern are empty strings. .. _v6.13.6: ------------------- 6.13.6 - 2021-05-26 ------------------- This patch fixes a bug that caused :func:`~hypothesis.strategies.integers` to shrink towards negative values instead of positive values in some cases. .. _v6.13.5: ------------------- 6.13.5 - 2021-05-24 ------------------- This patch fixes rare cases where ``hypothesis write --binary-op`` could print :ref:`reproducing instructions ` from the internal search for an identity element. .. _v6.13.4: ------------------- 6.13.4 - 2021-05-24 ------------------- This patch removes some unnecessary intermediate list-comprehensions, using the latest versions of :pypi:`pyupgrade` and :pypi:`shed`. .. _v6.13.3: ------------------- 6.13.3 - 2021-05-23 ------------------- This patch adds a ``.hypothesis`` property to invalid test functions, bringing them inline with valid tests and fixing a bug where :pypi:`pytest-asyncio` would swallow the real error message and mistakenly raise a version incompatibility error. .. _v6.13.2: ------------------- 6.13.2 - 2021-05-23 ------------------- Some of Hypothesis's numpy/pandas strategies use a ``fill`` argument to speed up generating large arrays, by generating a single fill value and sharing that value among many array slots instead of filling every single slot individually. When no ``fill`` argument is provided, Hypothesis tries to detect whether it is OK to automatically use the ``elements`` argument as a fill strategy, so that it can still use the faster approach. This patch fixes a bug that would cause that optimization to trigger in some cases where it isn't 100% guaranteed to be OK. If this makes some of your numpy/pandas tests run more slowly, try adding an explicit ``fill`` argument to the relevant strategies to ensure that Hypothesis always uses the faster approach. .. _v6.13.1: ------------------- 6.13.1 - 2021-05-20 ------------------- This patch strengthens some internal import-time consistency checks for the built-in strategies. There is no user-visible change. .. _v6.13.0: ------------------- 6.13.0 - 2021-05-18 ------------------- This release adds URL fragment generation to the :func:`~hypothesis.provisional.urls` strategy (:issue:`2908`). Thanks to Pax (R. Margret) for contributing this patch at the `PyCon US Mentored Sprints `__! .. _v6.12.1: ------------------- 6.12.1 - 2021-05-17 ------------------- This patch fixes :issue:`2964`, where ``.map()`` and ``.filter()`` methods were omitted from the ``repr()`` of :func:`~hypothesis.strategies.just` and :func:`~hypothesis.strategies.sampled_from` strategies, since :ref:`version 5.43.7 `. .. _v6.12.0: ------------------- 6.12.0 - 2021-05-06 ------------------- This release automatically rewrites some simple filters, such as ``integers().filter(lambda x: x > 9)`` to the more efficient ``integers(min_value=10)``, based on the AST of the predicate. We continue to recommend using the efficient form directly wherever possible, but this should be useful for e.g. :pypi:`pandera` "``Checks``" where you already have a simple predicate and translating manually is really annoying. See :issue:`2701` for ideas about floats and simple text strategies. .. _v6.11.0: ------------------- 6.11.0 - 2021-05-06 ------------------- :func:`hypothesis.target` now returns the ``observation`` value, allowing it to be conveniently used inline in expressions such as ``assert target(abs(a - b)) < 0.1``. .. _v6.10.1: ------------------- 6.10.1 - 2021-04-26 ------------------- This patch fixes a deprecation warning if you're using recent versions of :pypi:`importlib-metadata` (:issue:`2934`), which we use to load :ref:`third-party plugins ` such as `Pydantic's integration `__. On older versions of :pypi:`importlib-metadata`, there is no change and you don't need to upgrade. .. _v6.10.0: ------------------- 6.10.0 - 2021-04-17 ------------------- This release teaches the :ref:`Ghostwriter ` to read parameter types from Sphinx, Google, or Numpy-style structured docstrings, and improves some related heuristics about how to test scientific and numerical programs. .. _v6.9.2: ------------------ 6.9.2 - 2021-04-15 ------------------ This release improves the :ref:`Ghostwriter's ` handling of exceptions, by reading ``:raises ...:`` entries in function docstrings and ensuring that we don't suppresss the error raised by test assertions. .. _v6.9.1: ------------------ 6.9.1 - 2021-04-12 ------------------ This patch updates our autoformatting tools, improving our code style without any API changes. .. _v6.9.0: ------------------ 6.9.0 - 2021-04-11 ------------------ This release teaches :func:`~hypothesis.strategies.from_type` how to see through :obj:`python:typing.Annotated`. Thanks to Vytautas Strimaitis for reporting and fixing :issue:`2919`! .. _v6.8.12: ------------------- 6.8.12 - 2021-04-11 ------------------- If :pypi:`rich` is installed, the :command:`hypothesis write` command will use it to syntax-highlight the :ref:`Ghostwritten ` code. .. _v6.8.11: ------------------- 6.8.11 - 2021-04-11 ------------------- This patch improves an error message from :func:`~hypothesis.strategies.builds` when :func:`~hypothesis.strategies.from_type` would be more suitable (:issue:`2930`). .. _v6.8.10: ------------------- 6.8.10 - 2021-04-11 ------------------- This patch updates the type annotations for :func:`~hypothesis.extra.numpy.arrays` to reflect that ``shape: SearchStrategy[int]`` is supported. .. _v6.8.9: ------------------ 6.8.9 - 2021-04-07 ------------------ This patch fixes :func:`~hypothesis.strategies.from_type` with :mod:`abstract types ` which have either required but non-type-annotated arguments to ``__init__``, or where :func:`~hypothesis.strategies.from_type` can handle some concrete subclasses but not others. .. _v6.8.8: ------------------ 6.8.8 - 2021-04-07 ------------------ This patch teaches :command:`hypothesis write` to check for possible roundtrips in several more cases, such as by looking for an inverse in the module which defines the function to test. .. _v6.8.7: ------------------ 6.8.7 - 2021-04-07 ------------------ This patch adds a more helpful error message if you try to call :func:`~hypothesis.strategies.sampled_from` on an :class:`~python:enum.Enum` which has no members, but *does* have :func:`~python:dataclasses.dataclass`-style annotations (:issue:`2923`). .. _v6.8.6: ------------------ 6.8.6 - 2021-04-06 ------------------ The :func:`~hypothesis.strategies.fixed_dictionaries` strategy now preserves dict iteration order instead of sorting the keys. This also affects the pretty-printing of keyword arguments to :func:`@given() ` (:issue:`2913`). .. _v6.8.5: ------------------ 6.8.5 - 2021-04-05 ------------------ This patch teaches :command:`hypothesis write` to default to ghostwriting tests with ``--style=pytest`` only if :pypi:`pytest` is installed, or ``--style=unittest`` otherwise. .. _v6.8.4: ------------------ 6.8.4 - 2021-04-01 ------------------ This patch adds type annotations for the :class:`~hypothesis.settings` decorator, to avoid an error when running mypy in strict mode. .. _v6.8.3: ------------------ 6.8.3 - 2021-03-28 ------------------ This patch improves the :ref:`Ghostwriter's ` handling of strategies to generate various fiddly types including frozensets, keysviews, valuesviews, regex matches and patterns, and so on. .. _v6.8.2: ------------------ 6.8.2 - 2021-03-27 ------------------ This patch fixes some internal typos. There is no user-visible change. .. _v6.8.1: ------------------ 6.8.1 - 2021-03-14 ------------------ This patch lays more groundwork for filter rewriting (:issue:`2701`). There is no user-visible change... yet. .. _v6.8.0: ------------------ 6.8.0 - 2021-03-11 ------------------ This release :func:`registers ` the remaining builtin types, and teaches :func:`~hypothesis.strategies.from_type` to try resolving :class:`~python:typing.ForwardRef` and :class:`~python:typing.Type` references to built-in types. .. _v6.7.0: ------------------ 6.7.0 - 2021-03-10 ------------------ This release teaches :class:`~hypothesis.stateful.RuleBasedStateMachine` to avoid checking :func:`~hypothesis.stateful.invariant`\ s until all :func:`~hypothesis.stateful.initialize` rules have been run. You can enable checking of specific invariants for incompletely initialized machines by using ``@invariant(check_during_init=True)`` (:issue:`2868`). In previous versions, it was possible if awkward to implement this behaviour using :func:`~hypothesis.stateful.precondition` and an auxiliary variable. .. _v6.6.1: ------------------ 6.6.1 - 2021-03-09 ------------------ This patch improves the error message when :func:`~hypothesis.strategies.from_type` fails to resolve a forward-reference inside a :class:`python:typing.Type` such as ``Type["int"]`` (:issue:`2565`). .. _v6.6.0: ------------------ 6.6.0 - 2021-03-07 ------------------ This release makes it an explicit error to apply :func:`~hypothesis.stateful.invariant` to a :func:`~hypothesis.stateful.rule` or :func:`~hypothesis.stateful.initialize` rule in :ref:`stateful testing `. Such a combination had unclear semantics, especially in combination with :func:`~hypothesis.stateful.precondition`, and was never meant to be allowed (:issue:`2681`). .. _v6.5.0: ------------------ 6.5.0 - 2021-03-07 ------------------ This release adds |Phase.explain|, in which Hypothesis attempts to explain *why* your test failed by pointing to suspicious lines of code (i.e. those which were always, and only, run on failing inputs). We plan to include "generalising" failing examples in this phase in a future release (:issue:`2192`). .. _v6.4.3: ------------------ 6.4.3 - 2021-03-04 ------------------ This patch fixes :issue:`2794`, where nesting :func:`~hypothesis.strategies.deferred` strategies within :func:`~hypothesis.strategies.recursive` strategies could trigger an internal assertion. While it was always possible to get the same results from a more sensible strategy, the convoluted form now works too. .. _v6.4.2: ------------------ 6.4.2 - 2021-03-04 ------------------ This patch fixes several problems with ``mypy`` when `--no-implicit-reexport `_ was activated in user projects. Thanks to Nikita Sobolev for fixing :issue:`2884`! .. _v6.4.1: ------------------ 6.4.1 - 2021-03-04 ------------------ This patch fixes an exception that occurs when using type unions of the :pypi:`typing-extensions` ``Literal`` backport on Python 3.6. Thanks to Ben Anhalt for identifying and fixing this bug. .. _v6.4.0: ------------------ 6.4.0 - 2021-03-02 ------------------ This release fixes :ref:`stateful testing methods ` with multiple :func:`~hypothesis.stateful.precondition` decorators. Previously, only the outer-most precondition was checked (:issue:`2681`). .. _v6.3.4: ------------------ 6.3.4 - 2021-02-28 ------------------ This patch refactors some internals of :class:`~hypothesis.stateful.RuleBasedStateMachine`. There is no change to the public API or behaviour. .. _v6.3.3: ------------------ 6.3.3 - 2021-02-26 ------------------ This patch moves some internal code, so that future work can avoid creating import cycles. There is no user-visible change. .. _v6.3.2: ------------------ 6.3.2 - 2021-02-25 ------------------ This patch enables :func:`~hypothesis.strategies.register_type_strategy` for subclasses of :obj:`python:typing.TypedDict`. Previously, :func:`~hypothesis.strategies.from_type` would ignore the registered strategy (:issue:`2872`). Thanks to Ilya Lebedev for identifying and fixing this bug! .. _v6.3.1: ------------------ 6.3.1 - 2021-02-24 ------------------ This release lays the groundwork for automatic rewriting of simple filters, for example converting ``integers().filter(lambda x: x > 9)`` to ``integers(min_value=10)``. Note that this is **not supported yet**, and we will continue to recommend writing the efficient form directly wherever possible - predicate rewriting is provided mainly for the benefit of downstream libraries which would otherwise have to implement it for themselves (e.g. :pypi:`pandera` and :pypi:`icontract-hypothesis`). See :issue:`2701` for details. .. _v6.3.0: ------------------ 6.3.0 - 2021-02-20 ------------------ The Hypothesis :pypi:`pytest` plugin now requires pytest version 4.6 or later. If the plugin detects an earlier version of pytest, it will automatically deactivate itself. `(4.6.x is the earliest pytest branch that still accepts community bugfixes.) `__ Hypothesis-based tests should continue to work in earlier versions of pytest, but enhanced integrations provided by the plugin (such as ``--hypothesis-show-statistics`` and other command-line flags) will no longer be available in obsolete pytest versions. .. _v6.2.0: ------------------ 6.2.0 - 2021-02-12 ------------------ If you use :pypi:`pytest-html`, Hypothesis now includes the :ref:`summary statistics for each test ` in the HTML report, whether or not the ``--hypothesis-show-statistics`` argument was passed to show them in the command-line output. .. _v6.1.1: ------------------ 6.1.1 - 2021-01-31 ------------------ This patch updates our automatic code formatting to use :pypi:`shed`, which includes :pypi:`autoflake`, :pypi:`black`, :pypi:`isort`, and :pypi:`pyupgrade` (:issue:`2780`). .. _v6.1.0: ------------------ 6.1.0 - 2021-01-29 ------------------ This release teaches Hypothesis to distinguish between errors based on the `__cause__ or __context__ of otherwise identical exceptions `__, which is particularly useful when internal errors can be wrapped by a library-specific or semantically appropriate exception such as: .. code-block:: python try: do_the_thing(foo, timeout=10) except Exception as err: raise FooError("Failed to do the thing") from err Earlier versions of Hypothesis only see the ``FooError``, while we can now distinguish a ``FooError`` raised because of e.g. an internal assertion from one raised because of a ``TimeoutExceeded`` exception. .. _v6.0.4: ------------------ 6.0.4 - 2021-01-27 ------------------ This release prevents a race condition inside :func:`~hypothesis.strategies.recursive` strategies. The race condition occurs when the same :func:`~hypothesis.strategies.recursive` strategy is shared among tests that are running in multiple threads (:issue:`2717`). .. _v6.0.3: ------------------ 6.0.3 - 2021-01-23 ------------------ This patch improves the type annotations for :func:`~hypothesis.strategies.one_of`, by adding overloads to handle up to five distinct arguments as :obj:`~python:typing.Union` before falling back to :obj:`~python:typing.Any`, as well as annotating the ``|`` (``__or__``) operator for strategies (:issue:`2765`). .. _v6.0.2: ------------------ 6.0.2 - 2021-01-14 ------------------ This release makes some small improvements to how filtered strategies work. It should improve the performance of shrinking filtered strategies, and may under some (probably rare) circumstances improve the diversity of generated examples. .. _v6.0.1: ------------------ 6.0.1 - 2021-01-13 ------------------ This patch fixes an interaction where our :ref:`test statistics ` handling made Pytest's ``--junit-xml`` output fail to validate against the strict ``xunit2`` schema (:issue:`1975`). .. _v6.0.0: ------------------ 6.0.0 - 2021-01-08 ------------------ Welcome to the next major version of Hypothesis! There are no new features here, as we release those in minor versions. Instead, 6.0 is a chance for us to remove deprecated features (many already converted into no-ops), and turn a variety of warnings into errors. If you were running on the last version of Hypothesis 5.x *without any Hypothesis deprecation warnings*, this will be a very boring upgrade. **In fact, nothing will change for you at all.** Changes ~~~~~~~ - Many functions now use :pep:`3102` keyword-only arguments where passing positional arguments :ref:`was deprecated since 5.5 `. - :func:`hypothesis.extra.django.from_model` no longer accepts ``model`` as a keyword argument, where it could conflict with fields named "model". - :func:`~hypothesis.strategies.randoms` now defaults to ``use_true_random=False``. - :func:`~hypothesis.strategies.complex_numbers` no longer accepts ``min_magnitude=None``; either use ``min_magnitude=0`` or just omit the argument. - ``hypothesis.provisional.ip4_addr_strings`` and ``ip6_addr_strings`` are removed in favor of :func:`ip_addresses(v=...).map(str) `. - :func:`~hypothesis.strategies.register_type_strategy` no longer accepts generic types with type arguments, which were always pretty badly broken. - Using function-scoped pytest fixtures is now a health-check error, instead of a warning. .. tip:: The :command:`hypothesis codemod` command can automatically refactor your code, particularly to convert positional to keyword arguments where those are now required. Hypothesis 5.x ============== .. _v5.49.0: ------------------- 5.49.0 - 2021-01-07 ------------------- This release adds the :obj:`~hypothesis.HealthCheck.function_scoped_fixture` health check value, which can be used to suppress the existing warning that appears when :func:`@given ` is applied to a test that uses pytest function-scoped fixtures. (This warning exists because function-scoped fixtures only run once per function, not once per example, which is usually unexpected and can cause subtle problems.) When this warning becomes a health check error in a future release, suppressing it via Python warning settings will no longer be possible. In the rare case that once-per-function behaviour is intended, it will still be possible to use :obj:`~hypothesis.HealthCheck.function_scoped_fixture` to opt out of the health check error for specific tests. .. _v5.48.0: ------------------- 5.48.0 - 2021-01-06 ------------------- This release adds :func:`hypothesis.currently_in_test_context`, which can be used to check whether the calling code is currently running inside an :func:`@given ` or :ref:`stateful ` test. This is most useful for third-party integrations and assertion helpers which may wish to use :func:`~hypothesis.assume` or :func:`~hypothesis.target`, without also requiring that the helper only be used from property-based tests (:issue:`2581`). .. _v5.47.0: ------------------- 5.47.0 - 2021-01-05 ------------------- This release upgrades the import logic for :ref:`ghostwritten tests `, handling many cases where imports would previously be missing or from unexpected locations. .. _v5.46.0: ------------------- 5.46.0 - 2021-01-04 ------------------- This release upgrades :func:`~hypothesis.strategies.from_type`, to infer strategies for type-annotated arguments even if they have defaults when it otherwise falls back to :func:`~hypothesis.strategies.builds` (:issue:`2708`). .. _v5.45.0: ------------------- 5.45.0 - 2021-01-04 ------------------- This release adds the :ref:`codemods` extra, which you can use to check for and automatically fix issues such as use of deprecated Hypothesis APIs (:issue:`2705`). .. _v5.44.0: ------------------- 5.44.0 - 2021-01-03 ------------------- This patch fixes :func:`~hypothesis.strategies.from_type` with the :pypi:`typing-extensions` ``Literal`` backport on Python 3.6. .. _v5.43.9: ------------------- 5.43.9 - 2021-01-02 ------------------- This patch fixes :issue:`2722`, where certain orderings of :func:`~hypothesis.strategies.register_type_strategy`, :class:`~python:typing.ForwardRef`, and :func:`~hypothesis.strategies.from_type` could trigger an internal error. .. _v5.43.8: ------------------- 5.43.8 - 2021-01-02 ------------------- This patch makes some strategies for collections with a uniqueness constraint much more efficient, including ``dictionaries(keys=sampled_from(...), values=..)`` and ``lists(tuples(sampled_from(...), ...), unique_by=lambda x: x[0])``. (related to :issue:`2036`) .. _v5.43.7: ------------------- 5.43.7 - 2021-01-02 ------------------- This patch extends our faster special case for :func:`~hypothesis.strategies.sampled_from` elements in unique :func:`~hypothesis.strategies.lists` to account for chains of ``.map(...)`` and ``.filter(...)`` calls (:issue:`2036`). .. _v5.43.6: ------------------- 5.43.6 - 2021-01-02 ------------------- This patch improves the type annotations on :func:`~hypothesis.assume` and :func:`@reproduce_failure() `. .. _v5.43.5: ------------------- 5.43.5 - 2021-01-01 ------------------- This patch updates our copyright headers to include 2021. Happy new year! .. _v5.43.4: ------------------- 5.43.4 - 2020-12-24 ------------------- This change fixes a documentation error in the :obj:`~hypothesis.settings.database` setting. The previous documentation suggested that callers could specify a database path string, or the special string ``":memory:"``, but this setting has never actually allowed string arguments. Permitted values are ``None``, and instances of :class:`~hypothesis.database.ExampleDatabase`. .. _v5.43.3: ------------------- 5.43.3 - 2020-12-11 ------------------- This patch fixes :issue:`2696`, an internal error triggered when the :obj:`@example ` decorator was used and the :obj:`~hypothesis.settings.verbosity` setting was ``quiet``. .. _v5.43.2: ------------------- 5.43.2 - 2020-12-10 ------------------- This patch improves the error message from the :func:`~hypothesis.extra.pandas.data_frames` strategy when both the ``rows`` and ``columns`` arguments are given, but there is a missing entry in ``rows`` and the corresponding column has no ``fill`` value (:issue:`2678`). .. _v5.43.1: ------------------- 5.43.1 - 2020-12-10 ------------------- This patch improves the error message if :func:`~hypothesis.strategies.builds` is passed an :class:`~python:enum.Enum` which cannot be called without arguments, to suggest using :func:`~hypothesis.strategies.sampled_from` (:issue:`2693`). .. _v5.43.0: ------------------- 5.43.0 - 2020-12-09 ------------------- This release adds new :func:`~hypothesis.strategies.timezones` and :func:`~hypothesis.strategies.timezone_keys` strategies (:issue:`2630`) based on the new :mod:`python:zoneinfo` module in Python 3.9. ``pip install hypothesis[zoneinfo]`` will ensure that you have the appropriate backports installed if you need them. .. _v5.42.3: ------------------- 5.42.3 - 2020-12-09 ------------------- This patch fixes an internal error in :func:`~hypothesis.strategies.datetimes` with ``allow_imaginary=False`` where the ``timezones`` argument can generate ``tzinfo=None`` (:issue:`2662`). .. _v5.42.2: ------------------- 5.42.2 - 2020-12-09 ------------------- This patch teaches :func:`hypothesis.extra.django.from_field` to infer more efficient strategies by inspecting (not just filtering by) field validators for numeric and string fields (:issue:`1116`). .. _v5.42.1: ------------------- 5.42.1 - 2020-12-09 ------------------- This patch refactors :class:`hypothesis.settings` to use type-annotated keyword arguments instead of ``**kwargs``, which makes tab-completion much more useful - as well as type-checkers like :pypi:`mypy`. .. _v5.42.0: ------------------- 5.42.0 - 2020-12-09 ------------------- This patch teaches the :func:`~hypothesis.extra.ghostwriter.magic` ghostwriter to recognise "en/de" function roundtrips other than the common encode/decode pattern, such as encrypt/decrypt or, encipher/decipher. .. _v5.41.5: ------------------- 5.41.5 - 2020-12-05 ------------------- This patch adds a performance optimisation to avoid saving redundant seeds when using :ref:`the .fuzz_one_input hook `. .. _v5.41.4: ------------------- 5.41.4 - 2020-11-28 ------------------- This patch fixes :issue:`2657`, where passing unicode patterns compiled with :obj:`python:re.IGNORECASE` to :func:`~hypothesis.strategies.from_regex` could trigger an internal error when casefolding a character creates a longer string (e.g. ``"\u0130".lower() -> "i\u0370"``). .. _v5.41.3: ------------------- 5.41.3 - 2020-11-18 ------------------- This patch adds a final fallback clause to :ref:`our plugin logic ` to fail with a warning rather than error on Python < 3.8 when neither the :pypi:`importlib-metadata` (preferred) or :pypi:`setuptools` (fallback) packages are available. .. _v5.41.2: ------------------- 5.41.2 - 2020-11-08 ------------------- This patch fixes :func:`~hypothesis.provisional.urls` strategy ensuring that ``~`` (tilde) is treated as one of the url-safe characters (:issue:`2658`). .. _v5.41.1: ------------------- 5.41.1 - 2020-11-03 ------------------- This patch improves our :ref:`CLI help and documentation `. .. _v5.41.0: ------------------- 5.41.0 - 2020-10-30 ------------------- Hypothesis now shrinks examples where the error is raised while drawing from a strategy. This makes complicated custom strategies *much* easier to debug, at the cost of a slowdown for use-cases where you catch and ignore such errors. .. _v5.40.0: ------------------- 5.40.0 - 2020-10-30 ------------------- This release teaches :func:`~hypothesis.strategies.from_type` how to handle :class:`~python:typing.ChainMap`, :class:`~python:typing.Counter`, :class:`~python:typing.Deque`, :class:`~python:typing.Generator`, :class:`~python:typing.Match`, :class:`~python:typing.OrderedDict`, :class:`~python:typing.Pattern`, and :class:`~python:collections.abc.Set` (:issue:`2654`). .. _v5.39.0: ------------------- 5.39.0 - 2020-10-30 ------------------- :func:`~hypothesis.strategies.from_type` now knows how to resolve :pep:`585` parameterized standard collection types, which are new in Python 3.9 (:issue:`2629`). .. _v5.38.1: ------------------- 5.38.1 - 2020-10-26 ------------------- This patch fixes :func:`~hypothesis.strategies.builds`, so that when passed :obj:`~hypothesis.infer` for an argument with a non-:obj:`~python:typing.Optional` type annotation and a default value of ``None`` to build a class which defines an explicit ``__signature__`` attribute, either ``None`` or that type may be generated. This is unlikely to happen unless you are using :pypi:`pydantic` (:issue:`2648`). .. _v5.38.0: ------------------- 5.38.0 - 2020-10-24 ------------------- This release improves our support for :func:`@st.composite ` on a :obj:`python:classmethod` or :obj:`python:staticmethod` (:issue:`2578`). .. _v5.37.5: ------------------- 5.37.5 - 2020-10-24 ------------------- This patch fixes :func:`~hypothesis.strategies.from_type` with :class:`Iterable[T] ` (:issue:`2645`). .. _v5.37.4: ------------------- 5.37.4 - 2020-10-20 ------------------- This patch teaches the :func:`~hypothesis.extra.ghostwriter.magic` ghostwriter to recognise that pairs of functions like :func:`~python:colorsys.rgb_to_hsv` and :func:`~python:colorsys.hsv_to_rgb` should :func:`~hypothesis.extra.ghostwriter.roundtrip`. .. _v5.37.3: ------------------- 5.37.3 - 2020-10-15 ------------------- This patch improves :func:`~hypothesis.strategies.builds` and :func:`~hypothesis.strategies.from_type` support for explicitly defined ``__signature__`` attributes, from :ref:`version 5.8.3 `, to support generic types from the :mod:`python:typing` module. Thanks to Rónán Carrigan for identifying and fixing this problem! .. _v5.37.2: ------------------- 5.37.2 - 2020-10-14 ------------------- This patch fixes :func:`~hypothesis.extra.lark.from_lark` with version 0.10.1+ of the :pypi:`lark-parser` package. .. _v5.37.1: ------------------- 5.37.1 - 2020-10-07 ------------------- This patch fixes some broken links in the :mod:`~hypothesis.extra.lark` extra documentation. .. _v5.37.0: ------------------- 5.37.0 - 2020-10-03 ------------------- This release adds a new :class:`~hypothesis.extra.redis.RedisExampleDatabase`, along with the :class:`~hypothesis.database.ReadOnlyDatabase` and :class:`~hypothesis.database.MultiplexedDatabase` helpers, to support team workflows where failing examples can be seamlessly shared between everyone on the team - and your CI servers or buildbots. .. _v5.36.2: ------------------- 5.36.2 - 2020-10-02 ------------------- This patch ensures that if the :ref:`"hypothesis" entry point ` is callable, we call it after importing it. You can still use non-callable entry points (like modules), which are only imported. We also prefer `importlib.metadata `__ or :pypi:`the backport ` over `pkg_resources `__, which makes ``import hypothesis`` around 200 milliseconds faster (:issue:`2571`). .. _v5.36.1: ------------------- 5.36.1 - 2020-09-25 ------------------- This patch adds some helpful suggestions to error messages you might see while learning to use the :obj:`@example() ` decorator (:issue:`2611`) or the :func:`~hypothesis.strategies.one_of` strategy. .. _v5.36.0: ------------------- 5.36.0 - 2020-09-24 ------------------- This release upgrades the :func:`~hypothesis.extra.numpy.from_dtype` strategy to pass optional ``**kwargs`` to the inferred strategy, and upgrades the :func:`~hypothesis.extra.numpy.arrays` strategy to accept an ``elements=kwargs`` dict to pass through to :func:`~hypothesis.extra.numpy.from_dtype`. ``arrays(floating_dtypes(), shape, elements={"min_value": -10, "max_value": 10})`` is a particularly useful pattern, as it allows for any floating dtype without triggering the roundoff warning for smaller types or sacrificing variety for larger types (:issue:`2552`). .. _v5.35.4: ------------------- 5.35.4 - 2020-09-21 ------------------- This patch reformats our code with the latest :pypi:`black` to take advantage of the support for magic trailing commas. .. _v5.35.3: ------------------- 5.35.3 - 2020-09-15 ------------------- This release significantly improves the performance of Hypothesis's internal implementation of automaton learning. However this code does not run as part of the user-accessible API so this has no user-visible impact. .. _v5.35.2: ------------------- 5.35.2 - 2020-09-14 ------------------- This patch ensures that, when the ``generate`` :obj:`~hypothesis.settings.phases` is disabled, we can replay up to :obj:`~hypothesis.settings.max_examples` examples from the database - which is very useful when :ref:`using Hypothesis with a fuzzer `. Thanks to Afrida Tabassum for fixing :issue:`2585`! .. _v5.35.1: ------------------- 5.35.1 - 2020-09-14 ------------------- This patch changes some internal :obj:`python:struct.Struct.format` strings from ``bytes`` to ``str``, to avoid :class:`python:BytesWarning` when running `python -bb `__. Thanks to everyone involved in `pytest-xdist issue 596 `__, :bpo:`16349`, :bpo:`21071`, and :bpo:`41777` for their work on this - it was a remarkably subtle issue! .. _v5.35.0: ------------------- 5.35.0 - 2020-09-11 ------------------- The :func:`~hypothesis.target` function now accepts integers as well as floats. .. _v5.34.1: ------------------- 5.34.1 - 2020-09-11 ------------------- This patch adds explicit :obj:`~python:typing.Optional` annotations to our public API, to better support users who run :pypi:`mypy` with ``--strict`` or ``no_implicit_optional=True``. Thanks to Krzysztof Przybyła for bringing this to our attention and writing the patch! .. _v5.34.0: ------------------- 5.34.0 - 2020-09-11 ------------------- This release drops support for Python 3.5, which `reached end of life upstream `__ on 2020-09-13. .. _v5.33.2: ------------------- 5.33.2 - 2020-09-09 ------------------- This patch fixes a problem with :func:`~hypothesis.strategies.builds` that was not able to generate valid data for annotated classes with constructors. Thanks to Nikita Sobolev for fixing :issue:`2603`! .. _v5.33.1: ------------------- 5.33.1 - 2020-09-07 ------------------- This patch improves the error message from the :command:`hypothesis write` command if :pypi:`black` (required for the :ref:`ghostwriter `) is not installed. Thanks to Nikita Sobolev for fixing :issue:`2604`! .. _v5.33.0: ------------------- 5.33.0 - 2020-09-06 ------------------- When reporting failing examples, or tried examples in verbose mode, Hypothesis now identifies which were from :obj:`@example(...) ` explicit examples. .. _v5.32.1: ------------------- 5.32.1 - 2020-09-06 ------------------- This patch contains some internal refactoring. Thanks to Felix Sheldon for fixing :issue:`2516`! .. _v5.32.0: ------------------- 5.32.0 - 2020-09-04 ------------------- An array drawn from :func:`~hypothesis.extra.numpy.arrays` will own its own memory; previously most arrays returned by this strategy were views. .. _v5.31.0: ------------------- 5.31.0 - 2020-09-04 ------------------- :func:`~hypothesis.strategies.builds` will use the ``__signature__`` attribute of the target, if it exists, to retrieve type hints. Previously :func:`python:typing.get_type_hints`, was used by default. If argument names varied between the ``__annotations__`` and ``__signature__``, they would not be supplied to the target. This was particularly an issue for :pypi:`pydantic` models which use an `alias generator `__. .. _v5.30.1: ------------------- 5.30.1 - 2020-09-04 ------------------- This patch makes the :ref:`ghostwriter ` much more robust when passed unusual modules. - improved support for non-resolvable type annotations - :func:`~hypothesis.extra.ghostwriter.magic` can now write :func:`~hypothesis.extra.ghostwriter.equivalent` tests - running :func:`~hypothesis.extra.ghostwriter.magic` on modules where some names in ``__all__`` are undefined skips such names, instead of raising an error - :func:`~hypothesis.extra.ghostwriter.magic` now knows to skip mocks - improved handling of import-time errors found by the ghostwriter CLI .. _v5.30.0: ------------------- 5.30.0 - 2020-08-30 ------------------- :func:`~hypothesis.strategies.register_type_strategy` now supports :class:`python:typing.TypeVar`, which was previously hard-coded, and allows a variety of types to be generated for an unconstrained :class:`~python:typing.TypeVar` instead of just :func:`~hypothesis.strategies.text`. Thanks again to Nikita Sobolev for all your work on advanced types! .. _v5.29.4: ------------------- 5.29.4 - 2020-08-28 ------------------- This release fixes some hard to trigger bugs in Hypothesis's automata learning code. This code is only run as part of the Hypothesis build process, and not for user code, so this release has no user visible impact. .. _v5.29.3: ------------------- 5.29.3 - 2020-08-27 ------------------- This patch adds type annotations to the :ref:`hypothesis.database ` module. There is no runtime change, but your typechecker might notice. .. _v5.29.2: ------------------- 5.29.2 - 2020-08-27 ------------------- This patch tracks some additional information in Hypothesis internals, and has no user-visible impact. .. _v5.29.1: ------------------- 5.29.1 - 2020-08-27 ------------------- This release fixes a bug in some Hypothesis internal support code for learning automata. This mostly doesn't have any user visible impact, although it slightly affects the learned shrink passes so shrinking may be subtly different. .. _v5.29.0: ------------------- 5.29.0 - 2020-08-24 ------------------- This release adds support for :ref:`entry-points`, which allows for smoother integration of third-party Hypothesis extensions and external libraries. Unless you're publishing a library with Hypothesis integration, you'll probably only ever use this indirectly! .. _v5.28.0: ------------------- 5.28.0 - 2020-08-24 ------------------- :func:`~hypothesis.strategies.from_type` can now resolve :class:`~python:typing.TypeVar` instances when the ``bound`` is a :class:`~python:typing.ForwardRef`, so long as that name is in fact defined in the same module as the typevar (no ``TYPE_CHECKING`` tricks, sorry). This feature requires Python 3.7 or later. Thanks to Zac Hatfield-Dodds and Nikita Sobolev for this feature! .. _v5.27.0: ------------------- 5.27.0 - 2020-08-20 ------------------- This patch adds two new :ref:`ghostwriters ` to test :wikipedia:`binary operations `, like :func:`python:operator.add`, and Numpy :doc:`ufuncs ` and :doc:`gufuncs ` like :data:`np.matmul() `. .. _v5.26.1: ------------------- 5.26.1 - 2020-08-19 ------------------- This release improves the performance of some methods in Hypothesis's internal automaton library. These are currently only lightly used by user code, but this may result in slightly faster shrinking. .. _v5.26.0: ------------------- 5.26.0 - 2020-08-17 ------------------- :func:`~hypothesis.strategies.register_type_strategy` no longer accepts parametrised user-defined generic types, because the resolution logic was quite badly broken (:issue:`2537`). Instead of registering a strategy for e.g. ``MyCollection[int]``, you should register a *function* for ``MyCollection`` and `inspect the type parameters within that function `__. Thanks to Nikita Sobolev for the bug report, design assistance, and pull request to implement this feature! .. _v5.25.0: ------------------- 5.25.0 - 2020-08-16 ------------------- Tired of writing tests? Or new to Hypothesis and not sure where to start? This release is for you! With our new :ref:`Ghostwriter functions ` and :command:`hypothesis write ...` :ref:`command-line interface `, you can stop writing tests entirely... or take the source code Hypothesis writes for you as a starting point. This has been in the works for months, from :issue:`2118` to versions :ref:`5.18.3 `, :ref:`5.23.5 `, and :ref:`5.23.5 ` - particular thanks to the many people who reviewed pull requests or commented on demos, and to Timothy Crosley's :pypi:`hypothesis-auto` project for inspiration. .. _v5.24.4: ------------------- 5.24.4 - 2020-08-14 ------------------- This patch adds yet more internal functions to support a new feature we're working on, like :ref:`version 5.18.3 ` and :ref:`version 5.23.6 `. We promise it's worth the wait! .. _v5.24.3: ------------------- 5.24.3 - 2020-08-13 ------------------- This release fixes a small internal bug in Hypothesis's internal automaton library. Fortunately this bug was currently impossible to hit in user facing code, so this has no user visible impact. .. _v5.24.2: ------------------- 5.24.2 - 2020-08-12 ------------------- This release improves shrink quality by allowing Hypothesis to automatically learn new shrink passes for difficult to shrink tests. The automatic learning is not currently accessible in user code (it still needs significant work on robustness and performance before it is ready for that), but this release includes learned passes that should improve shrinking quality for tests which use any of the :func:`~hypothesis.strategies.text`, :func:`~hypothesis.strategies.floats`, :func:`~hypothesis.strategies.datetimes`, :func:`~hypothesis.strategies.emails`, and :func:`~hypothesis.strategies.complex_numbers` strategies. .. _v5.24.1: ------------------- 5.24.1 - 2020-08-12 ------------------- This patch updates some docstrings, without changing runtime behaviour. .. _v5.24.0: ------------------- 5.24.0 - 2020-08-10 ------------------- The :func:`~hypothesis.strategies.functions` strategy has a new argument ``pure=True``, which ensures that the same return value is generated for identical calls to the generated function (:issue:`2538`). Thanks to Zac Hatfield-Dodds and Nikita Sobolev for this feature! .. _v5.23.12: -------------------- 5.23.12 - 2020-08-10 -------------------- This release removes a number of Hypothesis's internal "shrink passes" - transformations it makes to a generated test case during shrinking - which appeared to be redundant with other transformations. It is unlikely that you will see much impact from this. If you do, it will likely show up as a change in shrinking performance (probably slower, maybe faster), or possibly in worse shrunk results. If you encounter the latter, please let us know. .. _v5.23.11: -------------------- 5.23.11 - 2020-08-04 -------------------- This release fixes a bug in some internal Hypothesis support code. It has no user visible impact. .. _v5.23.10: -------------------- 5.23.10 - 2020-08-04 -------------------- This release improves the quality of shrunk test cases in some special cases. Specifically, it should get shrinking unstuck in some scenarios which require simultaneously changing two parts of the generated test case. .. _v5.23.9: ------------------- 5.23.9 - 2020-08-03 ------------------- This release improves the performance of some internal support code. It has no user visible impact, as that code is not currently run during normal Hypothesis operation. .. _v5.23.8: ------------------- 5.23.8 - 2020-07-31 ------------------- This release adds a heuristic to detect when shrinking has finished despite the fact that there are many more possible transformations to try. This will be particularly useful for tests where the minimum failing test case is very large despite there being many smaller test cases possible, where it is likely to speed up shrinking dramatically. In some cases it is likely that this will result in worse shrunk test cases. In those cases rerunning the test will result in further shrinking. .. _v5.23.7: ------------------- 5.23.7 - 2020-07-29 ------------------- This release makes some performance improvements to shrinking. They should only be noticeable for tests that are currently particularly slow to shrink. .. _v5.23.6: ------------------- 5.23.6 - 2020-07-29 ------------------- This patch adds some more internal functions to support a new feature we're working on, like :ref:`version 5.18.3 `. There is still no user-visible change... yet. .. _v5.23.5: ------------------- 5.23.5 - 2020-07-29 ------------------- This release makes some changes to internal support code that is not currently used in production Hypothesis. It has no user visible effect at present. .. _v5.23.4: ------------------- 5.23.4 - 2020-07-29 ------------------- This release improves shrinking quality in some special cases. .. _v5.23.3: ------------------- 5.23.3 - 2020-07-27 ------------------- This release fixes :issue:`2507`, where lazy evaluation meant that the values drawn from a :func:`~hypothesis.strategies.sampled_from` strategy could depend on mutations of the sampled sequence that happened after the strategy was constructed. .. _v5.23.2: ------------------- 5.23.2 - 2020-07-27 ------------------- This patch fixes :issue:`2462`, a bug in our handling of :meth:`unittest.TestCase.subTest`. Thanks to Israel Fruchter for fixing this at the EuroPython sprints! .. _v5.23.1: ------------------- 5.23.1 - 2020-07-26 ------------------- This release improves the behaviour of the :func:`~hypothesis.strategies.characters` strategy when shrinking, by changing which characters are considered smallest to prefer more "normal" ascii characters where available. .. _v5.23.0: ------------------- 5.23.0 - 2020-07-26 ------------------- The default ``print_blob`` setting is now smarter. It defaults to ``True`` in CI and ``False`` for local development. Thanks to Hugo van Kemenade for implementing this feature at the EuroPython sprints! .. _v5.22.0: ------------------- 5.22.0 - 2020-07-25 ------------------- The :func:`~hypothesis.strategies.slices` strategy can now generate slices for empty sequences, slices with negative start and stop indices (from the end of the sequence), and ``step=None`` in place of ``step=1``. Thanks to Sangarshanan for implementing this feature at the EuroPython sprints! .. _v5.21.0: ------------------- 5.21.0 - 2020-07-23 ------------------- This release ensures that tests which raise ``RecursionError`` are not reported as flaky simply because we run them from different initial stack depths (:issue:`2494`). .. _v5.20.4: ------------------- 5.20.4 - 2020-07-23 ------------------- This release improves the performance of the ``sample`` method on objects obtained from :func:`~hypothesis.strategies.randoms` when ``use_true_random=False``. This should mostly only be noticeable when the sample size is a large fraction of the population size, but may also help avoid health check failures in some other cases. .. _v5.20.3: ------------------- 5.20.3 - 2020-07-21 ------------------- This release makes some internal changes for testing purposes and should have no user visible effect. .. _v5.20.2: ------------------- 5.20.2 - 2020-07-18 ------------------- This release fixes a small caching bug in Hypothesis internals that may under some circumstances have resulted in a less diverse set of test cases being generated than was intended. Fixing this problem revealed some performance problems that could occur during targeted property based testing, so this release also fixes those. Targeted property-based testing should now be significantly faster in some cases, but this may be at the cost of reduced effectiveness. .. _v5.20.1: ------------------- 5.20.1 - 2020-07-17 ------------------- This patch updates our formatting to use :pypi:`isort` 5. There is no user-visible change. .. _v5.20.0: ------------------- 5.20.0 - 2020-07-17 ------------------- The :func:`~hypothesis.extra.numpy.basic_indices` strategy can now generate bare indexers in place of length-one tuples. Thanks to Andrea for this patch! .. _v5.19.3: ------------------- 5.19.3 - 2020-07-15 ------------------- This patch removes an internal use of ``distutils`` in order to avoid `this setuptools warning `__ for some users. .. _v5.19.2: ------------------- 5.19.2 - 2020-07-13 ------------------- This patch contains a small internal refactoring with no user-visible impact. Thanks to Andrea for writing this at `the SciPy 2020 Sprints `__! .. _v5.19.1: ------------------- 5.19.1 - 2020-07-12 ------------------- This release slightly improves shrinking behaviour. This should mainly only impact stateful tests, but may have some minor positive impact on shrinking collections (lists, sets, etc). .. _v5.19.0: ------------------- 5.19.0 - 2020-06-30 ------------------- This release improves the :func:`~hypothesis.strategies.randoms` strategy by adding support for ``Random`` instances where Hypothesis generates the random values rather than having them be "truly" random. .. _v5.18.3: ------------------- 5.18.3 - 2020-06-27 ------------------- This patch adds some internal functions to support a new feature we're working on. There is no user-visible change... yet. .. _v5.18.2: ------------------- 5.18.2 - 2020-06-26 ------------------- This patch improves our docs for the :obj:`~hypothesis.settings.derandomize` setting. .. _v5.18.1: ------------------- 5.18.1 - 2020-06-25 ------------------- This release consists of some internal refactoring to the shrinker in preparation for future work. It has no user visible impact. .. _v5.18.0: ------------------- 5.18.0 - 2020-06-22 ------------------- This release teaches Hypothesis to :ref:`shorten tracebacks ` for |@example|, as we already do for generated examples, so that you can focus on your code rather than ours. If you have multiple failing explicit examples, they will now all be reported. To report only the first failure, you can use the :obj:`report_multiple_bugs=False ` setting as for generated examples. .. _v5.17.0: ------------------- 5.17.0 - 2020-06-22 ------------------- This patch adds strategy inference for the ``Literal``, ``NewType``, ``Type``, ``DefaultDict``, and ``TypedDict`` types from the :pypi:`typing-extensions` backport on PyPI. .. _v5.16.3: ------------------- 5.16.3 - 2020-06-21 ------------------- This patch precomputes some of the setup logic for our :ref:`external fuzzer integration ` and sets :obj:`deadline=None ` in fuzzing mode, saving around 150us on each iteration. This is around two-thirds the runtime to fuzz an empty test with ``@given(st.none())``, and nice to have even as a much smaller fraction of the runtime for non-trivial tests. .. _v5.16.2: ------------------- 5.16.2 - 2020-06-19 ------------------- This patch fixes an internal error when warning about the use of function-scoped fixtures for parametrised tests where the parametrised value contained a ``%`` character. Thanks to Bryant for reporting and fixing this bug! .. _v5.16.1: ------------------- 5.16.1 - 2020-06-10 ------------------- If you pass a :class:`python:list` or :class:`python:tuple` where a strategy was expected, the error message now mentions :func:`~hypothesis.strategies.sampled_from` as an example strategy. Thanks to the enthusiastic participants in the `PyCon Mentored Sprints `__ who suggested adding this hint. .. _v5.16.0: ------------------- 5.16.0 - 2020-05-27 ------------------- :func:`~hypothesis.strategies.functions` can now infer the appropriate ``returns`` strategy if you pass a ``like`` function with a return-type annotation. Before, omitting the ``returns`` argument would generate functions that always returned None. .. _v5.15.1: ------------------- 5.15.1 - 2020-05-21 ------------------- Fix :func:`~hypothesis.strategies.from_type` with generic types under Python 3.9. .. _v5.15.0: ------------------- 5.15.0 - 2020-05-19 ------------------- This patch fixes an error that happens when multiple threads create new strategies. .. _v5.14.0: ------------------- 5.14.0 - 2020-05-13 ------------------- Passing ``min_magnitude=None`` to :func:`~hypothesis.strategies.complex_numbers` is now deprecated - you can explicitly pass ``min_magnitude=0``, or omit the argument entirely. .. _v5.13.1: ------------------- 5.13.1 - 2020-05-13 ------------------- This patch fixes an internal error in :func:`~hypothesis.strategies.from_type` for :obj:`python:typing.NamedTuple` in Python 3.9. Thanks to Michel Salim for reporting and fixing :issue:`2427`! .. _v5.13.0: ------------------- 5.13.0 - 2020-05-12 ------------------- This release upgrades the test statistics available via the :ref:`--hypothesis-show-statistics ` option to include separate information on each of the :attr:`~hypothesis.settings.phases` (:issue:`1555`). .. _v5.12.2: ------------------- 5.12.2 - 2020-05-12 ------------------- This patch teaches the :func:`~hypothesis.strategies.from_type` internals to return slightly more efficient strategies for some generic sets and mappings. .. _v5.12.1: ------------------- 5.12.1 - 2020-05-12 ------------------- This patch adds a ``# noqa`` comment for :pypi:`flake8` 3.8.0, which disagrees with :pypi:`mypy` about how to write the type of ``...``. .. _v5.12.0: ------------------- 5.12.0 - 2020-05-10 ------------------- This release limits the maximum duration of the shrinking phase to five minutes, so that Hypothesis does not appear to hang when making very slow progress shrinking a failing example (:issue:`2340`). If one of your tests triggers this logic, we would really appreciate a bug report to help us improve the shrinker for difficult but realistic workloads. .. _v5.11.0: ------------------- 5.11.0 - 2020-05-07 ------------------- This release improves the interaction between :func:`~hypothesis.assume` and the :obj:`@example() ` decorator, so that the following test no longer fails with ``UnsatisfiedAssumption`` (:issue:`2125`): .. code-block:: python @given(value=floats(0, 1)) @example(value=0.56789) # used to make the test fail! @pytest.mark.parametrize("threshold", [0.5, 1]) def test_foo(threshold, value): assume(value < threshold) ... .. _v5.10.5: ------------------- 5.10.5 - 2020-05-04 ------------------- If you have :pypi:`Django` installed but don't use it, this patch will make ``import hypothesis`` a few hundred milliseconds faster (e.g. 0.704s -> 0.271s). Thanks to :pypi:`importtime-waterfall` for highlighting this problem and `Jake Vanderplas `__ for the solution - it's impossible to misuse code from a module you haven't imported! .. _v5.10.4: ------------------- 5.10.4 - 2020-04-24 ------------------- This patch improves the internals of :func:`~hypothesis.strategies.builds` type inference, to handle recursive forward references in certain dataclasses. This is useful for e.g. :pypi:`hypothesmith`'s forthcoming :pypi:`LibCST ` mode. .. _v5.10.3: ------------------- 5.10.3 - 2020-04-22 ------------------- This release reverses the order in which some operations are tried during shrinking. This should generally be a slight performance improvement, but most tests are unlikely to notice much difference. .. _v5.10.2: ------------------- 5.10.2 - 2020-04-22 ------------------- This patch fixes :issue:`2406`, where use of :obj:`pandas:pandas.Timestamp` objects as bounds for the :func:`~hypothesis.strategies.datetimes` strategy caused an internal error. This bug was introduced in :ref:`version 5.8.1 `. .. _v5.10.1: ------------------- 5.10.1 - 2020-04-19 ------------------- This release is a small internal refactoring to how shrinking interacts with :ref:`targeted property-based testing ` that should have no user visible impact. .. _v5.10.0: ------------------- 5.10.0 - 2020-04-18 ------------------- This release improves our support for datetimes and times around DST transitions. :func:`~hypothesis.strategies.times` and :func:`~hypothesis.strategies.datetimes` are now sometimes generated with ``fold=1``, indicating that they represent the second occurrence of a given wall-time when clocks are set backwards. This may be set even when there is no transition, in which case the ``fold`` value should be ignored. For consistency, timezones provided by the :pypi:`pytz` package can now generate imaginary times (such as the hour skipped over when clocks 'spring forward' to daylight saving time, or during some historical timezone transitions). All other timezones have always supported generation of imaginary times. If you prefer the previous behaviour, :func:`~hypothesis.strategies.datetimes` now takes an argument ``allow_imaginary`` which defaults to ``True`` but can be set to ``False`` for any timezones strategy. .. _v5.9.1: ------------------ 5.9.1 - 2020-04-16 ------------------ This patch fixes the rendering of :func:`~hypothesis.strategies.binary` docstring by using the proper backticks syntax. .. _v5.9.0: ------------------ 5.9.0 - 2020-04-15 ------------------ Failing tests which use :func:`~hypothesis.target` now report the highest score observed for each target alongside the failing example(s), even without :ref:`explicitly showing test statistics `. This improves the debugging workflow for tests of accuracy, which assert that the total imprecision is within some error budget - for example, ``abs(a - b) < 0.5``. Previously, shrinking to a minimal failing example could often make errors seem smaller or more subtle than they really are (see `the threshold problem `__, and :issue:`2180`). .. _v5.8.6: ------------------ 5.8.6 - 2020-04-15 ------------------ This patch improves the docstring of :func:`~hypothesis.strategies.binary`, the :func:`python:repr` of :func:`~hypothesis.strategies.sampled_from` on an :class:`python:enum.Enum` subclass, and a warning in our pytest plugin. There is no change in runtime behaviour. .. _v5.8.5: ------------------ 5.8.5 - 2020-04-15 ------------------ This release (potentially very significantly) improves the performance of failing tests in some rare cases, mostly only relevant when using :ref:`targeted property-based testing `, by stopping further optimisation of unrelated test cases once a failing example is found. .. _v5.8.4: ------------------ 5.8.4 - 2020-04-14 ------------------ This release fixes :issue:`2395`, where under some circumstances targeted property-based testing could cause Hypothesis to get caught in an infinite loop. .. _v5.8.3: ------------------ 5.8.3 - 2020-04-12 ------------------ This patch teaches :func:`~hypothesis.strategies.builds` and :func:`~hypothesis.strategies.from_type` to use the ``__signature__`` attribute of classes where it has been set, improving our support for :pypi:`pydantic` models (`in pydantic >= 1.5 `__). .. _v5.8.2: ------------------ 5.8.2 - 2020-04-12 ------------------ This release improves the performance of the part of the core engine that deliberately generates duplicate values. .. _v5.8.1: ------------------ 5.8.1 - 2020-04-12 ------------------ This patch improves :func:`~hypothesis.strategies.dates` shrinking, to simplify year, month, and day like :func:`~hypothesis.strategies.datetimes` rather than minimizing the number of days since 2000-01-01. .. _v5.8.0: ------------------ 5.8.0 - 2020-03-24 ------------------ This release adds a :ref:`.hypothesis.fuzz_one_input ` attribute to :func:`@given ` tests, for easy integration with external fuzzers such as `python-afl `__ (supporting :issue:`171`). .. _v5.7.2: ------------------ 5.7.2 - 2020-03-24 ------------------ This patch fixes :issue:`2341`, ensuring that the printed output from a stateful test cannot use variable names before they are defined. .. _v5.7.1: ------------------ 5.7.1 - 2020-03-23 ------------------ This patch fixes :issue:`2375`, preventing incorrect failure when a function scoped fixture is overridden with a higher scoped fixture. .. _v5.7.0: ------------------ 5.7.0 - 2020-03-19 ------------------ This release allows the :func:`~hypothesis.extra.numpy.array_dtypes` strategy to generate Numpy dtypes which have `field titles in addition to field names `__. We expect this to expose latent bugs where code expects that ``set(dtype.names) == set(dtype.fields)``, though the latter may include titles. .. _v5.6.1: ------------------ 5.6.1 - 2020-03-18 ------------------ This makes ``model`` a positional-only argument to :func:`~hypothesis.extra.django.from_model`, to support models with a field literally named "model" (:issue:`2369`). .. _v5.6.0: ------------------ 5.6.0 - 2020-02-29 ------------------ This release adds an explicit warning for tests that are both decorated with :func:`@given(...) ` and request a :doc:`function-scoped pytest fixture `, because such fixtures are only executed once for *all* Hypothesis test cases and that often causes trouble (:issue:`377`). It's *very* difficult to fix this on the :pypi:`pytest` side, so since 2015 our advice has been "just don't use function-scoped fixtures with Hypothesis". Now we detect and warn about the issue at runtime! .. _v5.5.5: ------------------ 5.5.5 - 2020-02-29 ------------------ This release cleans up the internal machinery for :ref:`stateful`, after we dropped the legacy APIs in Hypothesis 5.0 (:issue:`2218`). There is no user-visible change. .. _v5.5.4: ------------------ 5.5.4 - 2020-02-16 ------------------ This patch fixes :issue:`2351`, :func:`~hypothesis.extra.numpy.arrays` would raise a confusing error if we inferred a strategy for ``datetime64`` or ``timedelta64`` values with varying time units. We now infer an internally-consistent strategy for such arrays, and have a more helpful error message if an inconsistent strategy is explicitly specified. .. _v5.5.3: ------------------ 5.5.3 - 2020-02-14 ------------------ This patch improves the signature of :func:`~hypothesis.strategies.builds` by specifying ``target`` as a positional-only argument on Python 3.8 (see :pep:`570`). The semantics of :func:`~hypothesis.strategies.builds` have not changed at all - this just clarifies the documentation. .. _v5.5.2: ------------------ 5.5.2 - 2020-02-13 ------------------ This release makes Hypothesis faster at generating test cases that contain duplicated values in their inputs. .. _v5.5.1: ------------------ 5.5.1 - 2020-02-07 ------------------ This patch has some tiny internal code clean-ups, with no user-visible change. .. _v5.5.0: ------------------ 5.5.0 - 2020-02-07 ------------------ :gh-file:`Our style guide ` suggests that optional parameters should usually be keyword-only arguments (see :pep:`3102`) to prevent confusion based on positional arguments - for example, :func:`hypothesis.strategies.floats` takes up to *four* boolean flags and many of the Numpy strategies have both ``dims`` and ``side`` bounds. This release converts most optional parameters in our API to use keyword-only arguments - and adds a compatibility shim so you get warnings rather than errors everywhere (:issue:`2130`). .. _v5.4.2: ------------------ 5.4.2 - 2020-02-06 ------------------ This patch fixes compatibility with Python 3.5.2 (:issue:`2334`). Note that :doc:`we only test the latest patch of each minor version `, though as in this case we usually accept pull requests for older patch versions. .. _v5.4.1: ------------------ 5.4.1 - 2020-02-01 ------------------ This patch improves the repr of :func:`~hypothesis.strategies.from_type`, so that in most cases it will display the strategy it resolves to rather than ``from_type(...)``. The latter form will continue to be used where resolution is not immediately successful, e.g. invalid arguments or recursive type definitions involving forward references. .. _v5.4.0: ------------------ 5.4.0 - 2020-01-30 ------------------ This release removes support for Python 3.5.0 and 3.5.1, where the :mod:`python:typing` module was quite immature (e.g. missing :func:`~python:typing.overload` and :obj:`~python:typing.Type`). Note that Python 3.5 will reach its end-of-life in September 2020, and new releases of Hypothesis may drop support somewhat earlier. .. note:: ``pip install hypothesis`` should continue to give you the latest compatible version. If you have somehow ended up with an incompatible version, you need to update your packaging stack to ``pip >= 9.0`` and ``setuptools >= 24.2`` - see `here for details `__. Then ``pip uninstall hypothesis && pip install hypothesis`` will get you back to a compatible version. .. _v5.3.1: ------------------ 5.3.1 - 2020-01-26 ------------------ This patch does some minor internal cleanup; there is no user-visible change. .. _v5.3.0: ------------------ 5.3.0 - 2020-01-21 ------------------ The standard library :mod:`ipaddress` module is new in Python 3, and this release adds the new :func:`~hypothesis.strategies.ip_addresses` strategy to generate :class:`~python:ipaddress.IPv4Address`\ es and/or :class:`~python:ipaddress.IPv6Address`\ es (depending on the ``v`` and ``network`` arguments). If you use them in type annotations, :func:`~hypothesis.strategies.from_type` now has strategies registered for :mod:`ipaddress` address, network, and interface types. The provisional strategies for IP address strings are therefore deprecated. .. _v5.2.1: ------------------ 5.2.1 - 2020-01-21 ------------------ This patch reverts :ref:`version 5.2 `, due to a `strange issue `__ where indexing an array of strings can raise an error instead of returning an item which contains certain surrogate characters. .. _v5.2.0: ------------------ 5.2.0 - 2020-01-19 ------------------ This release allows :func:`~hypothesis.extra.numpy.from_dtype` to generate Unicode strings which cannot be encoded in UTF-8, but are valid in Numpy arrays (which use UTF-32). .. _v5.1.6: ------------------ 5.1.6 - 2020-01-19 ------------------ This patch fixes :issue:`2320`, where ``from_type(Set[Hashable])`` could raise an internal error because ``Decimal("snan")`` is of a hashable type, but raises an error when hashed. We now ensure that set elements and dict keys in generic types can actually be hashed. .. _v5.1.5: ------------------ 5.1.5 - 2020-01-12 ------------------ This patch fixes an internal error when running in an :pypi:`ipython` repl or :pypi:`jupyter` notebook on Windows (:issue:`2319`), and an internal error on Python 3.5.1 (:issue:`2318`). .. _v5.1.4: ------------------ 5.1.4 - 2020-01-11 ------------------ This patch fixes a bug where errors in third-party extensions such as :pypi:`hypothesis-trio` or :pypi:`hypothesis-jsonschema` were incorrectly considered to be Hypothesis internal errors, which could result in confusing error messages. Thanks to Vincent Michel for reporting and fixing the bug! .. _v5.1.3: ------------------ 5.1.3 - 2020-01-11 ------------------ This release converts the type hint comments on our public API to :pep:`484` type annotations. Thanks to Ivan Levkivskyi for :pypi:`com2ann` - with the refactoring tools from :ref:`5.0.1 ` it made this process remarkably easy! .. _v5.1.2: ------------------ 5.1.2 - 2020-01-09 ------------------ This patch makes :func:`~hypothesis.stateful.multiple` iterable, so that output like ``a, b = state.some_rule()`` is actually executable and can be used to reproduce failing examples. Thanks to Vincent Michel for reporting and fixing :issue:`2311`! .. _v5.1.1: ------------------ 5.1.1 - 2020-01-06 ------------------ This patch contains many small refactorings to replace our Python 2 compatibility functions with their native Python 3 equivalents. Since Hypothesis is now Python 3 only, there is no user-visible change. .. _v5.1.0: ------------------ 5.1.0 - 2020-01-03 ------------------ This release teaches :func:`~hypothesis.strategies.from_type` how to generate :class:`python:datetime.timezone`. As a result, you can now generate :class:`python:datetime.tzinfo` objects without having :pypi:`pytz` installed. If your tests specifically require :pypi:`pytz` timezones, you should be using :func:`hypothesis.extra.pytz.timezones` instead of ``st.from_type(tzinfo)``. .. _v5.0.1: ------------------ 5.0.1 - 2020-01-01 ------------------ This patch contains mostly-automated refactorings to remove code that we only needed to support Python 2. Since Hypothesis is now Python 3 only (hurray!), there is no user-visible change. Our sincere thanks to the authors of :pypi:`autoflake`, :pypi:`black`, :pypi:`isort`, and :pypi:`pyupgrade`, who have each and collectively made this kind of update enormously easier. .. _v5.0.0: ------------------ 5.0.0 - 2020-01-01 ------------------ Welcome to the next major version of Hypothesis! There are no new features here, as we release those in minor versions. Instead, 5.0 is a chance for us to remove deprecated features (many already converted into no-ops), and turn a variety of warnings into errors. If you were running on the last version of Hypothesis 4.x *without any Hypothesis deprecation warnings*, this will be a very boring upgrade. **In fact, nothing will change for you at all.** .. note:: This release drops support for Python 2, which has passed `its end of life date `__. The `Python 3 Statement `__ outlines our reasons, and lists many other packages that have made the same decision. ``pip install hypothesis`` should continue to give you the latest compatible version. If you have somehow ended up with Hypothesis 5.0 on Python 2, you need to update your packaging stack to ``pip >= 9.0`` and ``setuptools >= 24.2`` - see `here for details `__. Then ``pip uninstall hypothesis && pip install hypothesis`` will get you back to a compatible version. Strategies ~~~~~~~~~~ - :func:`~hypothesis.strategies.integers` bounds must be equal to an integer, though they can still be other types. - If :func:`~hypothesis.strategies.fractions` is passed a ``max_denominator``, the bounds must have at most that denominator. - :func:`~hypothesis.strategies.floats` bounds must be exactly representable as a floating-point number with the given ``width``. If not, the error message includes the nearest such number. - :func:`sampled_from([]) ` is now an error. - The values from the ``elements`` and ``fill`` strategies for :func:`hypothesis.extra.numpy.arrays` must be losslessly representable in an array of the given dtype. - The ``min_size`` and ``max_size`` arguments to all collection strategies must be of type :class:`python:int` (or ``max_size`` may be ``None``). Miscellaneous ~~~~~~~~~~~~~ - The ``.example()`` method of strategies (intended for interactive exploration) no longer takes a ``random`` argument. - It is now an error to apply :obj:`@example `, :func:`@seed `, or :func:`@reproduce_failure ` without also applying :func:`@given `. - You may pass either the ``target`` or ``targets`` argument to stateful rules, but not both. - :obj:`~hypothesis.settings.deadline` must be ``None`` (to disable), a :class:`~python:datetime.timedelta`, or an integer or float number of milliseconds. - Both of :obj:`~hypothesis.settings.derandomize` and :obj:`~hypothesis.settings.print_blob` must be either ``True`` or ``False``, where they previously accepted other values. - :obj:`~hypothesis.settings.stateful_step_count` must be at least one. - :obj:`~hypothesis.settings.max_examples` must be at least one. To disable example generation, use the :obj:`~hypothesis.settings.phases` setting. Removals ~~~~~~~~ - ``hypothesis.stateful.GenericStateMachine`` in favor of :class:`hypothesis.stateful.RuleBasedStateMachine` - ``hypothesis.extra.django.models.models`` in favor of :func:`hypothesis.extra.django.from_model` and ``hypothesis.extra.django.models.add_default_field_mapping`` in favor of :func:`hypothesis.extra.django.register_field_strategy` - ``hypothesis.HealthCheck.hung_test``, without replacement - ``hypothesis.settings.buffer``, without replacement - ``hypothesis.PrintSettings``, because :obj:`hypothesis.settings.print_blob` takes ``True`` or ``False`` - ``hypothesis.settings.timeout``, in favor of :obj:`hypothesis.settings.deadline` - ``hypothesis.unlimited`` without replacement (only useful as argument to ``timeout``) Hypothesis 4.x ============== .. _v4.57.1: ------------------- 4.57.1 - 2019-12-29 ------------------- This patch improves the type hints and documentation for the :ref:`django extra. ` There is no runtime change. .. _v4.57.0: ------------------- 4.57.0 - 2019-12-28 ------------------- This release improves support for the SupportsOp protocols from the :mod:`python:typing` module when using on :func:`~hypothesis.strategies.from_type` as outlined in :issue:`2292`. The following types now generate much more varied strategies when called with :func:`~hypothesis.strategies.from_type`: - :class:`python:typing.SupportsAbs` - :class:`python:typing.SupportsBytes` - :class:`python:typing.SupportsComplex` - :class:`python:typing.SupportsInt` - :class:`python:typing.SupportsFloat` - :class:`python:typing.SupportsRound` Note that using :func:`~hypothesis.strategies.from_type` with one of the above strategies will not ensure that the specified function will execute successfully (ie : the strategy returned for ``from_type(typing.SupportsAbs)`` may include NaNs or things which cause the :func:`python:abs` function to error. ) Thanks to Lea Provenzano for this patch. .. _v4.56.3: ------------------- 4.56.3 - 2019-12-22 ------------------- This release fixes a small internal bug in shrinking which could have caused it to perform slightly more tests than were necessary. Fixing this shouldn't have much effect but it will make shrinking slightly faster. .. _v4.56.2: ------------------- 4.56.2 - 2019-12-21 ------------------- This release removes an internal heuristic that was no longer providing much benefit. It is unlikely that there will be any user visible effect. .. _v4.56.1: ------------------- 4.56.1 - 2019-12-19 ------------------- This release further improves the optimisation algorithm for :ref:`targeted property-based testing `. .. _v4.56.0: ------------------- 4.56.0 - 2019-12-18 ------------------- This release enables deprecation warnings even when the :obj:`~hypothesis.settings.verbosity` setting is ``quiet``, in preparation for Hypothesis 5.0 (:issue:`2218`). Warnings can still be filtered by the standard mechanisms provided in the standard-library :mod:`python:warnings` module. .. _v4.55.4: ------------------- 4.55.4 - 2019-12-18 ------------------- This release improves Hypothesis's management of the set of test cases it tracks between runs. It will only do anything if you have the :obj:`~hypothesis.Phase.target` phase enabled and an example database set. In those circumstances it should result in a more thorough and faster set of examples that are tried on each run. .. _v4.55.3: ------------------- 4.55.3 - 2019-12-18 ------------------- This release makes Hypothesis better at generating test cases where generated values are duplicated in different parts of the test case. This will be especially noticeable with reasonably complex values, as it was already able to do this for simpler ones such as integers or floats. .. _v4.55.2: ------------------- 4.55.2 - 2019-12-17 ------------------- This release expands the set of test cases that Hypothesis saves in its database for future runs to include a representative set of "structurally different" test cases - e.g. it might try to save test cases where a given list is empty or not. Currently this is unlikely to have much user visible impact except to produce slightly more consistent behaviour between consecutive runs of a test suite. It is mostly groundwork for future improvements which will exploit this functionality more effectively. .. _v4.55.1: ------------------- 4.55.1 - 2019-12-16 ------------------- This patch fixes :issue:`2257`, where :func:`~hypothesis.strategies.from_type` could incorrectly generate bytestrings when passed a generic :class:`python:typing.Sequence` such as ``Sequence[set]``. .. _v4.55.0: ------------------- 4.55.0 - 2019-12-16 ------------------- This release adds database support for :ref:`targeted property-based testing `, so the best examples based on the targeting will be saved and reused between runs. This is mostly laying groundwork for future features in this area, but will also make targeted property-based tests more useful during development, where the same tests tend to get run over and over again. If :obj:`~hypothesis.settings.max_examples` is large, this may increase memory usage significantly under some circumstances, but these should be relatively rare. This release also adds a dependency on the :pypi:`sortedcontainers` package. .. _v4.54.2: ------------------- 4.54.2 - 2019-12-16 ------------------- This release improves the optimisation algorithm for :ref:`targeted property-based testing `, so that it will find higher quality results more reliably. Specifically, in cases where it would previously have got near a local optimum, it will now tend to achieve the locally optimal value. .. _v4.54.1: ------------------- 4.54.1 - 2019-12-16 ------------------- This release is mostly internal changes in support of better testing of the core engine. You are unlikely to see much effect, although some internal heuristics have changed slightly. .. _v4.54.0: ------------------- 4.54.0 - 2019-12-15 ------------------- This release adds a dedicated phase for :ref:`targeted property-based testing `, and (somewhat) improves the targeting algorithm so that it will find higher quality results more reliably. This comes at a cost of making it more likely to get stuck in a local optimum. .. _v4.53.3: ------------------- 4.53.3 - 2019-12-15 ------------------- This patch fixes :func:`~hypothesis.strategies.from_type` with :class:`python:typing.Hashable` and :class:`python:typing.Sized`, which previously failed with an internal error on Python 3.7 or later. Thanks to Lea Provenzano for both reporting :issue:`2272` and writing the patch! .. _v4.53.2: ------------------- 4.53.2 - 2019-12-11 ------------------- This release reorganises a number of the Hypothesis internal modules into a package structure. If you are only depending on the public API it should have no effect. If you are depending on the internal API (which you shouldn't be, and which we don't guarantee compatibility on) you may have to rename some imports. .. _v4.53.1: ------------------- 4.53.1 - 2019-12-09 ------------------- This release changes the size distribution of the number of steps run in stateful testing: It will now almost always run the maximum number of steps permitted. .. _v4.53.0: ------------------- 4.53.0 - 2019-12-09 ------------------- :ref:`statistics` now include the best score seen for each label, which can help avoid `the threshold problem `__ when the minimal example shrinks right down to the threshold of failure (:issue:`2180`). .. _v4.52.0: ------------------- 4.52.0 - 2019-12-09 ------------------- This release changes the ``stateful_step_count`` setting to raise an error if set to ``0``. This is a backwards compatible change because a value of ``0`` would never have worked and attempting to run it would have resulted in an internal assertion error. .. _v4.51.1: ------------------- 4.51.1 - 2019-12-09 ------------------- This release makes a small internal change to the distribution of test cases. It is unlikely to have much user visible impact. .. _v4.51.0: ------------------- 4.51.0 - 2019-12-07 ------------------- This release deprecates use of :obj:`@example `, :func:`@seed `, or :func:`@reproduce_failure ` without :func:`@given `. Thanks to Nick Anyos for the patch! .. _v4.50.8: ------------------- 4.50.8 - 2019-12-05 ------------------- This patch makes certain uses of Bundles more efficient in stateful testing (:issue:`2078`). .. _v4.50.7: ------------------- 4.50.7 - 2019-12-05 ------------------- This release refactors some of Hypothesis's internal interfaces for representing data generation. It should have no user visible effect. .. _v4.50.6: ------------------- 4.50.6 - 2019-12-02 ------------------- This patch removes some old debugging helpers in our Numpy extra which have not been needed since :issue:`1963` and :issue:`2245`. .. _v4.50.5: ------------------- 4.50.5 - 2019-12-01 ------------------- This patch fixes :issue:`2229`, where Numpy arrays of unsized strings would only ever have strings of size one due to an interaction between our generation logic and Numpy's allocation strategy. .. _v4.50.4: ------------------- 4.50.4 - 2019-12-01 ------------------- This patch fixes a rare internal error in strategies for a list of unique items sampled from a short non-unique sequence (:issue:`2247`). The bug was discovered via :pypi:`hypothesis-jsonschema`. .. _v4.50.3: ------------------- 4.50.3 - 2019-12-01 ------------------- This release improves the error message when :func:`@settings ` tries to inherit settings from a ``parent`` argument that isn't a ``settings`` instance. .. _v4.50.2: ------------------- 4.50.2 - 2019-11-29 ------------------- This release improves Hypothesis's "Falsifying example" output, by breaking output across multiple lines where necessary, and by removing irrelevant information from the stateful testing output. .. _v4.50.1: ------------------- 4.50.1 - 2019-11-29 ------------------- This patch adds :pypi:`flake8-comprehensions` to our linter suite. There is no user-visible change - expect perhaps via some strange microbenchmarks - but certain parts of the code now have a clear and more consistent style. .. _v4.50.0: ------------------- 4.50.0 - 2019-11-28 ------------------- This release fixes some cases where we might previously have failed to run the validation logic for some strategies. As a result tests which would previously have been silently testing significantly less than they should may now start to raise ``InvalidArgument`` now that these errors are caught. .. _v4.49.0: ------------------- 4.49.0 - 2019-11-28 ------------------- This release significantly improves the data distribution in :ref:`rule based stateful testing `, by using a technique called `Swarm Testing (Groce, Alex, et al. "Swarm testing." Proceedings of the 2012 International Symposium on Software Testing and Analysis. ACM, 2012.) `_ to select which rules are run in any given test case. This should allow it to find many issues that it would previously have missed. This change is likely to be especially beneficial for stateful tests with large numbers of rules. .. _v4.48.1: ------------------- 4.48.1 - 2019-11-28 ------------------- This release adds some heuristics to test case generation that try to ensure that test cases generated early on will be relatively small. This fixes a bug introduced in :ref:`Hypothesis 4.42.0 ` which would cause occasional :obj:`~hypothesis.HealthCheck.too_slow` failures on some tests. .. _v4.48.0: ------------------- 4.48.0 - 2019-11-28 ------------------- This release revokes the deprecation of ``find``, as we've now rebuilt it on top of ``@given``, which means it has minimal maintenance burden and we're happy to support it. .. _v4.47.5: ------------------- 4.47.5 - 2019-11-28 ------------------- This release rebuilds ``find()`` on top of ``@given`` in order to have more code in common. It should have minimal user visible effect. .. _v4.47.4: ------------------- 4.47.4 - 2019-11-27 ------------------- This patch removes an internal compatibility shim that we no longer need. .. _v4.47.3: ------------------- 4.47.3 - 2019-11-26 ------------------- This patch fixes several typos in our docstrings and comments, with no change in behaviour. Thanks to Dmitry Dygalo for identifying and fixing them! .. _v4.47.2: ------------------- 4.47.2 - 2019-11-25 ------------------- This release fixes an internal issue where Hypothesis would sometimes generate test cases that were above its intended maximum size. This would only have happened rarely and probably would not have caused major problems when it did. Users of the new :ref:`targeted property-based testing ` might see minor impact (possibly slightly faster tests and slightly worse target scores), but only in the unlikely event that they were hitting this problem. Other users should not see any effect at all. .. _v4.47.1: ------------------- 4.47.1 - 2019-11-24 ------------------- This release removes some unused code from the core engine. There is no user-visible change. .. _v4.47.0: ------------------- 4.47.0 - 2019-11-24 ------------------- This release commonizes some code between running explicit examples and normal test execution. The main user visible impact of this is that deadlines are now enforced when running explicit examples. .. _v4.46.1: ------------------- 4.46.1 - 2019-11-23 ------------------- This patch ensures that a KeyboardInterrupt received during example generation is not treated as a mystery test failure but instead propagates to the top level, not recording the interrupted generation in the conjecture data tree. Thanks to Anne Archibald for identifying and fixing the problem. .. _v4.46.0: ------------------- 4.46.0 - 2019-11-22 ------------------- This release changes the behaviour of :func:`~hypothesis.strategies.floats` when excluding signed zeros - ``floats(max_value=0.0, exclude_max=True)`` can no longer generate ``-0.0`` nor the much rarer ``floats(min_value=-0.0, exclude_min=True)`` generate ``+0.0``. The correct interaction between signed zeros and exclusive endpoints was unclear; we now enforce the invariant that :func:`~hypothesis.strategies.floats` will never generate a value equal to an excluded endpoint (:issue:`2201`). If you prefer the old behaviour, you can pass ``floats(max_value=-0.0)`` or ``floats(min_value=0.0)`` which is exactly equivalent and has not changed. If you had *two* endpoints equal to zero, we recommend clarifying your tests by using :func:`~hypothesis.strategies.just` or :func:`~hypothesis.strategies.sampled_from` instead of :func:`~hypothesis.strategies.floats`. .. _v4.45.1: ------------------- 4.45.1 - 2019-11-20 ------------------- This patch improves the error message when invalid arguments are passed to :func:`~hypothesis.stateful.rule` or :func:`~hypothesis.stateful.invariant` (:issue:`2149`). Thanks to Benjamin Palmer for this bugfix! .. _v4.45.0: ------------------- 4.45.0 - 2019-11-20 ------------------- This release supports :obj:`python:typing.Final` and :obj:`python:typing.TypedDict` in :func:`~hypothesis.strategies.from_type`. .. _v4.44.5: ------------------- 4.44.5 - 2019-11-20 ------------------- This patch disables our :pypi:`pytest` plugin when running on versions of :pypi:`pytest` before 4.3, the oldest our plugin supports. Note that at time of writing the Pytest developers only support 4.6 and later! Hypothesis *tests* using :func:`@given() ` work on any test runner, but our integrations to e.g. avoid example database collisions when combined with ``@pytest.mark.parametrize`` eventually drop support for obsolete versions. .. _v4.44.4: ------------------- 4.44.4 - 2019-11-20 ------------------- This patch adds some internal comments and clarifications to the Hypothesis implementation. There is no user-visible change. .. _v4.44.3: ------------------- 4.44.3 - 2019-11-20 ------------------- This patch avoids importing test runners such as :pypi:`pytest`, :pypi:`unittest2`, or :pypi:`nose` solely to access their special "skip test" exception types - if the module is not in :obj:`sys.modules`, the exception can't be raised anyway. This fixes a problem where importing an otherwise unused module could cause spurious errors due to import-time side effects (and possibly ``-Werror``). .. _v4.44.2: ------------------- 4.44.2 - 2019-11-12 ------------------- This release fixes :func:`@given ` to only complain about missing keyword-only arguments if the associated test function is actually called. This matches the behaviour of other ``InvalidArgument`` errors produced by ``@given``. .. _v4.44.1: ------------------- 4.44.1 - 2019-11-11 ------------------- This patch allows Hypothesis to run in environments that do not specify a ``__file__``, such as a :mod:`python:zipapp` (:issue:`2196`). .. _v4.44.0: ------------------- 4.44.0 - 2019-11-11 ------------------- This release adds a ``signature`` argument to :func:`~hypothesis.extra.numpy.mutually_broadcastable_shapes` (:issue:`2174`), which allows us to generate shapes which are valid for functions like :data:`np.matmul() ` that require shapes which are not simply broadcastable. Thanks to everyone who has contributed to this feature over the last year, and a particular shout-out to Zac Hatfield-Dodds and Ryan Soklaski for :func:`~hypothesis.extra.numpy.mutually_broadcastable_shapes` and to Ryan Turner for the downstream :pypi:`hypothesis-gufunc` project. .. _v4.43.9: ------------------- 4.43.9 - 2019-11-11 ------------------- This patch fixes :issue:`2108`, where the first test using :func:`~hypothesis.strategies.data` to draw from :func:`~hypothesis.strategies.characters` or :func:`~hypothesis.strategies.text` would be flaky due to unreliable test timings. Time taken by lazy instantiation of strategies is now counted towards drawing from the strategy, rather than towards the deadline for the test function. .. _v4.43.8: ------------------- 4.43.8 - 2019-11-08 ------------------- This release ensures that the strategies passed to :func:`@given ` are properly validated when applied to a test method inside a test class. This should result in clearer error messages when some of those strategies are invalid. .. _v4.43.7: ------------------- 4.43.7 - 2019-11-08 ------------------- This release changes how Hypothesis manages its search space in cases where it generates redundant data. This should cause it to generate significantly fewer duplicated examples (especially with short integer ranges), and may cause it to produce more useful examples in some cases (especially ones where there is a significant amount of filtering). .. _v4.43.6: ------------------- 4.43.6 - 2019-11-07 ------------------- This patch refactors ``width`` handling in :func:`~hypothesis.strategies.floats`; you may notice small performance improvements but the main purpose is to enable work on :issue:`1704` (improving shrinking of bounded floats). .. _v4.43.5: ------------------- 4.43.5 - 2019-11-06 ------------------- This patch removes an unused internal flag. There is no user-visible change. .. _v4.43.4: ------------------- 4.43.4 - 2019-11-05 ------------------- This patch corrects the exception type and error message you get if you attempt to use :func:`~hypothesis.strategies.data` to draw from something which is not a strategy. This never worked, but the error is more helpful now. .. _v4.43.3: ------------------- 4.43.3 - 2019-11-05 ------------------- We've adopted :pypi:`flake8-bugbear` to check for a few more style issues, and this patch implements the minor internal cleanups it suggested. There is no user-visible change. .. _v4.43.2: ------------------- 4.43.2 - 2019-11-05 ------------------- This patch fixes the formatting of some documentation, but there is no change to any executed code. .. _v4.43.1: ------------------- 4.43.1 - 2019-11-04 ------------------- Python 3.8's new :obj:`python:typing.Literal` type - see :pep:`586` for details - is now supported in :func:`~hypothesis.strategies.from_type`. .. _v4.43.0: ------------------- 4.43.0 - 2019-11-04 ------------------- This release adds the strategy :func:`~hypothesis.extra.numpy.mutually_broadcastable_shapes`, which generates multiple array shapes that are mutually broadcast-compatible with an optional user-specified base-shape. This is a generalisation of :func:`~hypothesis.extra.numpy.broadcastable_shapes`. It relies heavily on non-public internals for performance when generating and shrinking examples. We intend to support generating shapes matching a ufunc signature in a future version (:issue:`2174`). Thanks to Ryan Soklaski, Zac Hatfield-Dodds, and @rdturnermtl who contributed to this new feature. .. _v4.42.10: -------------------- 4.42.10 - 2019-11-03 -------------------- This release fixes :func:`~hypothesis.strategies.from_type` when used with bounded or constrained :obj:`python:typing.TypeVar` objects (:issue:`2094`). Previously, distinct typevars with the same constraints would be treated as all single typevar, and in cases where a typevar bound was resolved to a union of subclasses this could result in mixed types being generated for that typevar. .. _v4.42.9: ------------------- 4.42.9 - 2019-11-03 ------------------- This patch ensures that the default value :func:`~hypothesis.extra.numpy.broadcastable_shapes` chooses for ``max_dims`` is always valid (at most 32), even if you pass ``min_dims=32``. .. _v4.42.8: ------------------- 4.42.8 - 2019-11-02 ------------------- This patch ensures that we only add profile information to the pytest header if running either pytest or Hypothesis in verbose mode, matching the `builtin cache plugin `__ (:issue:`2155`). .. _v4.42.7: ------------------- 4.42.7 - 2019-11-02 ------------------- This patch makes stateful step printing expand the result of a step into multiple variables when you return :func:`~hypothesis.stateful.multiple` (:issue:`2139`). Thanks to Joseph Weston for reporting and fixing this bug! .. _v4.42.6: ------------------- 4.42.6 - 2019-11-02 ------------------- This release fixes a bug (:issue:`2166`) where a Unicode character info cache file was generated but never used on subsequent test runs, causing tests to run more slowly than they should have. Thanks to Robert Knight for this bugfix! .. _v4.42.5: ------------------- 4.42.5 - 2019-11-01 ------------------- This patch corrects some internal documentation. There is no user-visible change. .. _v4.42.4: ------------------- 4.42.4 - 2019-11-01 ------------------- This release fixes a bug (:issue:`2160`) where decorators applied after :func:`@settings ` and before :func:`@given ` were ignored. Thanks to Tom Milligan for this bugfix! .. _v4.42.3: ------------------- 4.42.3 - 2019-10-30 ------------------- This release updates Hypothesis's formatting to the new version of :pypi:`black`, and has absolutely no user visible effect. .. _v4.42.2: ------------------- 4.42.2 - 2019-10-30 ------------------- This release fixes a bug in :func:`~hypothesis.strategies.recursive` which would have meant that in practice ``max_leaves`` was treated as if it was lower than it actually is - specifically it would be capped at the largest power of two smaller than it. It is now handled correctly. .. _v4.42.1: ------------------- 4.42.1 - 2019-10-30 ------------------- Python 3.8's new :class:`python:typing.SupportsIndex` type - see :pep:`357` for details - is now supported in :func:`~hypothesis.strategies.from_type`. Thanks to Grigorios Giannakopoulos for the patch! .. _v4.42.0: ------------------- 4.42.0 - 2019-10-27 ------------------- This release significantly simplifies Hypothesis's internal logic for data generation, by removing a number of heuristics of questionable or unproven value. The results of this change will vary significantly from test to test. Most test suites will see significantly faster data generation and lower memory usage. The "quality" of the generated data may go up or down depending on your particular test suites. If you see any significant regressions in Hypothesis's ability to find bugs in your code as a result of this release, please file an issue to let us know. Users of the new :ref:`targeted property-based testing ` functionality are reasonably likely to see *improvements* in data generation, as this release changes the search algorithm for targeted property based testing to one that is more likely to be productive than the existing approach. .. _v4.41.3: ------------------- 4.41.3 - 2019-10-21 ------------------- This patch is to ensure that our internals remain comprehensible to :pypi:`mypy` 0.740 - there is no user-visible change. .. _v4.41.2: ------------------- 4.41.2 - 2019-10-17 ------------------- This patch changes some internal hashes to SHA384, to better support users subject to FIPS-140. There is no user-visible API change. Thanks to Paul Kehrer for this contribution! .. _v4.41.1: ------------------- 4.41.1 - 2019-10-16 ------------------- This release makes ``--hypothesis-show-statistics`` much more useful for tests using a :class:`~hypothesis.stateful.RuleBasedStateMachine`, by simplifying the reprs so that events are aggregated correctly. .. _v4.41.0: ------------------- 4.41.0 - 2019-10-16 ------------------- This release upgrades the :func:`~hypothesis.strategies.fixed_dictionaries` strategy to support ``optional`` keys (:issue:`1913`). .. _v4.40.2: ------------------- 4.40.2 - 2019-10-16 ------------------- This release makes some minor internal changes in support of improving the Hypothesis test suite. It should not have any user visible impact. .. _v4.40.1: ------------------- 4.40.1 - 2019-10-14 ------------------- This release changes how Hypothesis checks if a parameter to a test function is a mock object. It is unlikely to have any noticeable effect, but may result in a small performance improvement, especially for test functions where a mock object is being passed as the first argument. .. _v4.40.0: ------------------- 4.40.0 - 2019-10-09 ------------------- This release fixes a bug where our example database logic did not distinguish between failing examples based on arguments from a ``@pytest.mark.parametrize(...)``. This could in theory cause data loss if a common failure overwrote a rare one, and in practice caused occasional file-access collisions in highly concurrent workloads (e.g. during a 300-way parametrize on 16 cores). For internal reasons this also involves bumping the minimum supported version of :pypi:`pytest` to 4.3 Thanks to Peter C Kroon for the Hacktoberfest patch! .. _v4.39.3: ------------------- 4.39.3 - 2019-10-09 ------------------- This patch improves our type hints on the :func:`~hypothesis.strategies.emails`, :func:`~hypothesis.strategies.functions`, :func:`~hypothesis.strategies.integers`, :func:`~hypothesis.strategies.iterables`, and :func:`~hypothesis.strategies.slices` strategies, as well as the ``.filter()`` method. There is no runtime change, but if you use :pypi:`mypy` or a similar type-checker on your tests the results will be a bit more precise. .. _v4.39.2: ------------------- 4.39.2 - 2019-10-09 ------------------- This patch improves the performance of unique collections such as :func:`~hypothesis.strategies.sets` of :func:`~hypothesis.strategies.just` or :func:`~hypothesis.strategies.booleans` strategies. They were already pretty good though, so you're unlikely to notice much! .. _v4.39.1: ------------------- 4.39.1 - 2019-10-09 ------------------- If a value in a dict passed to :func:`~hypothesis.strategies.fixed_dictionaries` is not a strategy, Hypothesis now tells you which one. .. _v4.39.0: ------------------- 4.39.0 - 2019-10-07 ------------------- This release adds the :func:`~hypothesis.extra.numpy.basic_indices` strategy, to generate `basic indexes `__ for arrays of the specified shape (:issue:`1930`). It generates tuples containing some mix of integers, :obj:`python:slice` objects, ``...`` (Ellipsis), and :obj:`numpy:numpy.newaxis`; which when used to index an array of the specified shape produce either a scalar or a shared-memory view of the array. Note that the index tuple may be longer or shorter than the array shape, and may produce a view with another dimensionality again! Thanks to Lampros Mountrakis, Ryan Soklaski, and Zac Hatfield-Dodds for their collaboration on this surprisingly subtle strategy! .. _v4.38.3: ------------------- 4.38.3 - 2019-10-04 ------------------- This patch defers creation of the ``.hypothesis`` directory until we have something to store in it, meaning that it will appear when Hypothesis is used rather than simply installed. Thanks to Peter C Kroon for the Hacktoberfest patch! .. _v4.38.2: ------------------- 4.38.2 - 2019-10-02 ------------------- This patch bumps our dependency on :pypi:`attrs` to ``>=19.2.0``; but there are no user-visible changes to Hypothesis. .. _v4.38.1: ------------------- 4.38.1 - 2019-10-01 ------------------- This is a comment-only patch which tells :pypi:`mypy` 0.730 to ignore some internal compatibility shims we use to support older Pythons. .. _v4.38.0: ------------------- 4.38.0 - 2019-10-01 ------------------- This release adds the :func:`hypothesis.target` function, which implements :ref:`targeted property-based testing ` (:issue:`1779`). By calling :func:`~hypothesis.target` in your test function, Hypothesis can do a hill-climbing search for bugs. If you can calculate a suitable metric such as the load factor or length of a queue, this can help you find bugs with inputs that are highly improbably from unguided generation - however good our heuristics, example diversity, and deduplication logic might be. After all, those features are at work in targeted PBT too! .. _v4.37.0: ------------------- 4.37.0 - 2019-09-28 ------------------- This release emits a warning if you use the ``.example()`` method of a strategy in a non-interactive context. :func:`~hypothesis.given` is a much better choice for writing tests, whether you care about performance, minimal examples, reproducing failures, or even just the variety of inputs that will be tested! .. _v4.36.2: ------------------- 4.36.2 - 2019-09-20 ------------------- This patch disables part of the :mod:`typing`-based inference for the :pypi:`attrs` package under Python 3.5.0, which has some incompatible internal details (:issue:`2095`). .. _v4.36.1: ------------------- 4.36.1 - 2019-09-17 ------------------- This patch fixes a bug in strategy inference for :pypi:`attrs` classes where Hypothesis would fail to infer a strategy for attributes of a generic type such as ``Union[int, str]`` or ``List[bool]`` (:issue:`2091`). Thanks to Jonathan Gayvallet for the bug report and this patch! .. _v4.36.0: ------------------- 4.36.0 - 2019-09-09 ------------------- This patch deprecates ``min_len`` or ``max_len`` of 0 in :func:`~hypothesis.extra.numpy.byte_string_dtypes` and :func:`~hypothesis.extra.numpy.unicode_string_dtypes`. The lower limit is now 1. Numpy uses a length of 0 in these dtypes to indicate an undetermined size, chosen from the data at array creation. However, as the :func:`~hypothesis.extra.numpy.arrays` strategy creates arrays before filling them, strings were truncated to 1 byte. .. _v4.35.1: ------------------- 4.35.1 - 2019-09-09 ------------------- This patch improves the messaging that comes from invalid size arguments to collection strategies such as :func:`~hypothesis.strategies.lists`. .. _v4.35.0: ------------------- 4.35.0 - 2019-09-04 ------------------- This release improves the :func:`~hypothesis.extra.lark.from_lark` strategy, tightening argument validation and adding the ``explicit`` argument to allow use with terminals that use ``@declare`` instead of a string or regular expression. This feature is required to handle features such as indent and dedent tokens in Python code, which can be generated with the :pypi:`hypothesmith` package. .. _v4.34.0: ------------------- 4.34.0 - 2019-08-23 ------------------- The :func:`~hypothesis.strategies.from_type` strategy now knows to look up the subclasses of abstract types, which cannot be instantiated directly. This is very useful for :pypi:`hypothesmith` to support :pypi:`libCST `. .. _v4.33.1: ------------------- 4.33.1 - 2019-08-21 ------------------- This patch works around a crash when an incompatible version of Numpy is installed under PyPy 5.10 (Python 2.7). If you are still using Python 2, please upgrade to Python 3 as soon as possible - it will be unsupported at the end of this year. .. _v4.33.0: ------------------- 4.33.0 - 2019-08-20 ------------------- This release improves the :func:`~hypothesis.provisional.domains` strategy, as well as the :func:`~hypothesis.provisional.urls` and the :func:`~hypothesis.strategies.emails` strategies which use it. These strategies now use the full IANA list of Top Level Domains and are correct as per :rfc:`1035`. Passing tests using these strategies may now fail. Thanks to `TechDragon `__ for this improvement. .. _v4.32.3: ------------------- 4.32.3 - 2019-08-05 ------------------- This patch tidies up the repr of several ``settings``-related objects, at runtime and in the documentation, and deprecates the undocumented edge case that ``phases=None`` was treated like ``phases=tuple(Phase)``. It *also* fixes :func:`~hypothesis.extra.lark.from_lark` with :pypi:`lark 0.7.2 ` and later. .. _v4.32.2: ------------------- 4.32.2 - 2019-07-30 ------------------- This patch updates some internal comments for :pypi:`mypy` 0.720. There is no user-visible impact. .. _v4.32.1: ------------------- 4.32.1 - 2019-07-29 ------------------- This release changes how the shrinker represents its progress internally. For large generated test cases this should result in significantly less memory usage and possibly faster shrinking. Small generated test cases may be slightly slower to shrink but this shouldn't be very noticeable. .. _v4.32.0: ------------------- 4.32.0 - 2019-07-28 ------------------- This release makes :func:`~hypothesis.extra.numpy.arrays` more pedantic about ``elements`` strategies that cannot be exactly represented as array elements. In practice, you will see new warnings if you were using a ``float16`` or ``float32`` dtype without passing :func:`~hypothesis.strategies.floats` the ``width=16`` or ``width=32`` arguments respectively. The previous behaviour could lead to silent truncation, and thus some elements being equal to an explicitly excluded bound (:issue:`1899`). .. _v4.31.1: ------------------- 4.31.1 - 2019-07-28 ------------------- This patch changes an internal use of MD5 to SHA hashes, to better support users subject to FIPS-140. There is no user-visible or API change. Thanks to Alex Gaynor for this patch. .. _v4.31.0: ------------------- 4.31.0 - 2019-07-24 ------------------- This release simplifies the logic of the :attr:`~hypothesis.settings.print_blob` setting by removing the option to set it to ``PrintSettings.INFER``. As a result the ``print_blob`` setting now takes a single boolean value, and the use of ``PrintSettings`` is deprecated. .. _v4.28.2: ------------------- 4.28.2 - 2019-07-14 ------------------- This patch improves the docstrings of several Hypothesis strategies, by clarifying markup and adding cross-references. There is no runtime change. Thanks to Elizabeth Williams and Serah Njambi Rono for their contributions at the SciPy 2019 sprints! .. _v4.28.1: ------------------- 4.28.1 - 2019-07-12 ------------------- This patch improves the behaviour of the :func:`~hypothesis.strategies.text` strategy when passed an ``alphabet`` which is not a strategy. The value is now interpreted as ``include_characters`` to :func:`~hypothesis.strategies.characters` instead of a sequence for :func:`~hypothesis.strategies.sampled_from`, which standardises the distribution of examples and the shrinking behaviour. You can get the previous behaviour by using ``lists(sampled_from(alphabet)).map("".map)`` instead. .. _v4.28.0: ------------------- 4.28.0 - 2019-07-11 ------------------- This release deprecates ``find()``. The ``.example()`` method is a better replacement if you want *an* example, and for the rare occasions where you want the *minimal* example you can get it from :func:`@given `. :func:`@given ` has steadily outstripped ``find()`` in both features and performance over recent years, and as we do not have the resources to maintain and test both we think it is better to focus on just one. .. _v4.27.0: ------------------- 4.27.0 - 2019-07-08 ------------------- This release refactors the implementation of the ``.example()`` method, to more accurately represent the data which will be generated by :func:`@given `. As a result, calling ``s.example()`` on an empty strategy ``s`` (such as :func:`~hypothesis.strategies.nothing`) now raises ``Unsatisfiable`` instead of the old ``NoExamples`` exception. .. _v4.26.4: ------------------- 4.26.4 - 2019-07-07 ------------------- This patch ensures that the Pandas extra will keep working when Python 3.8 removes abstract base classes from the top-level :obj:`python:collections` namespace. This also fixes the relevant warning in Python 3.7, but there is no other difference in behaviour and you do not need to do anything. .. _v4.26.3: ------------------- 4.26.3 - 2019-07-05 ------------------- This release fixes :issue:`2027`, by changing the way Hypothesis tries to generate distinct examples to be more efficient. This may result in slightly different data distribution, and should improve generation performance in general, but should otherwise have minimal user impact. .. _v4.26.2: ------------------- 4.26.2 - 2019-07-04 ------------------- This release fixes :issue:`1864`, where some simple tests would perform very slowly, because they would run many times with each subsequent run being progressively slower. They will now stop after a more reasonable number of runs without hitting this problem. Unless you are hitting exactly this issue, it is unlikely that this release will have any effect, but certain classes of custom generators that are currently very slow may become a bit faster, or start to trigger health check failures. .. _v4.26.1: ------------------- 4.26.1 - 2019-07-04 ------------------- This release adds the strategy :func:`~hypothesis.extra.numpy.integer_array_indices`, which generates tuples of Numpy arrays that can be used for `advanced indexing `_ to select an array of a specified shape. .. _v4.26.0: ------------------- 4.26.0 - 2019-07-04 ------------------- This release significantly improves the performance of drawing unique collections whose elements are drawn from :func:`~hypothesis.strategies.sampled_from` strategies. As a side effect, this detects an error condition that would previously have passed silently: When the ``min_size`` argument on a collection with distinct elements is greater than the number of elements being sampled, this will now raise an error. .. _v4.25.1: ------------------- 4.25.1 - 2019-07-03 ------------------- This release removes some defunct internal functionality that was only being used for testing. It should have no user visible impact. .. _v4.25.0: ------------------- 4.25.0 - 2019-07-03 ------------------- This release deprecates and disables the ``buffer_size`` setting, which should have been treated as a private implementation detail all along. We recommend simply deleting this settings argument. .. _v4.24.6: ------------------- 4.24.6 - 2019-06-26 ------------------- This patch makes :func:`~hypothesis.strategies.datetimes` more efficient, as it now handles short months correctly by construction instead of filtering. .. _v4.24.5: ------------------- 4.24.5 - 2019-06-23 ------------------- This patch improves the development experience by simplifying the tracebacks you will see when e.g. you have used the ``.map(...)`` method of a strategy and the mapped function raises an exception. No new exceptions can be raised, nor existing exceptions change anything but their traceback. We're simply using if-statements rather than exceptions for control flow in a certain part of the internals! .. _v4.24.4: ------------------- 4.24.4 - 2019-06-21 ------------------- This patch fixes :issue:`2014`, where our compatibility layer broke with version 3.7.4 of the :pypi:`typing` module backport on PyPI. This issue only affects Python 2. We remind users that Hypothesis, like many other packages, `will drop Python 2 support on 2020-01-01 `__ and already has several features that are only available on Python 3. .. _v4.24.3: ------------------- 4.24.3 - 2019-06-07 ------------------- This patch improves the implementation of an internal wrapper on Python 3.8 beta1 (and will break on the alphas; but they're not meant to be stable). On other versions, there is no change at all. Thanks to Daniel Hahler for the patch, and Victor Stinner for his work on :bpo:`37032` that made it possible. .. _v4.24.2: ------------------- 4.24.2 - 2019-06-06 ------------------- Deprecation messages for functions in ``hypothesis.extra.django.models`` now explicitly name the deprecated function to make it easier to track down usages. Thanks to Kristian Glass for this contribution! .. _v4.24.1: ------------------- 4.24.1 - 2019-06-04 ------------------- This patch fixes :issue:`1999`, a spurious bug raised when a :func:`@st.composite ` function was passed a keyword-only argument. Thanks to Jim Nicholls for his fantastic bug report. .. _v4.24.0: ------------------- 4.24.0 - 2019-05-29 ------------------- This release deprecates ``GenericStateMachine``, in favor of :class:`~hypothesis.stateful.RuleBasedStateMachine`. Rule-based stateful testing is significantly faster, especially during shrinking. If your use-case truly does not fit rule-based stateful testing, we recommend writing a custom test function which drives your specific control-flow using :func:`~hypothesis.strategies.data`. .. _v4.23.9: ------------------- 4.23.9 - 2019-05-28 ------------------- This patch fixes a very rare example database issue with file permissions. When running a test that uses both :func:`@given ` and ``pytest.mark.parametrize``, using :pypi:`pytest-xdist` on Windows, with failing examples in the database, two attempts to read a file could overlap and we caught ``FileNotFound`` but not other ``OSError``\ s. .. _v4.23.8: ------------------- 4.23.8 - 2019-05-26 ------------------- This patch has a minor cleanup of the internal engine. There is no user-visible impact. .. _v4.23.7: ------------------- 4.23.7 - 2019-05-26 ------------------- This patch clarifies some error messages when the test function signature is incompatible with the arguments to :func:`@given `, especially when the :obj:`@settings() ` decorator is also used (:issue:`1978`). .. _v4.23.6: ------------------- 4.23.6 - 2019-05-19 ------------------- This release adds the :pypi:`pyupgrade` fixer to our code style, for consistent use of dict and set literals and comprehensions. .. _v4.23.5: ------------------- 4.23.5 - 2019-05-16 ------------------- This release slightly simplifies a small part of the core engine. There is no user-visible change. .. _v4.23.4: ------------------- 4.23.4 - 2019-05-09 ------------------- Fixes a minor formatting issue the docstring of :func:`~hypothesis.strategies.from_type` .. _v4.23.3: ------------------- 4.23.3 - 2019-05-09 ------------------- Adds a recipe to the docstring of :func:`~hypothesis.strategies.from_type` that describes a means for drawing values for "everything except" a specified type. This recipe is especially useful for writing tests that perform input-type validation. .. _v4.23.2: ------------------- 4.23.2 - 2019-05-08 ------------------- This patch uses :pypi:`autoflake` to remove some pointless ``pass`` statements, which improves our workflow but has no user-visible impact. .. _v4.23.1: ------------------- 4.23.1 - 2019-05-08 ------------------- This patch fixes an OverflowError in :func:`from_type(xrange) ` on Python 2. It turns out that not only do the ``start`` and ``stop`` values have to fit in a C long, but so does ``stop - start``. We now handle this even on 32bit platforms, but remind users that Python2 will not be supported after 2019 without specific funding. .. _v4.23.0: ------------------- 4.23.0 - 2019-05-08 ------------------- This release implements the :func:`~hypothesis.strategies.slices` strategy, to generate slices of a length-``size`` sequence. Thanks to Daniel J. West for writing this patch at the PyCon 2019 sprints! .. _v4.22.3: ------------------- 4.22.3 - 2019-05-07 ------------------- This patch exposes :class:`~hypothesis.strategies.DataObject`, *solely* to support more precise type hints. Objects of this type are provided by :func:`~hypothesis.strategies.data`, and can be used to draw examples from strategies intermixed with your test code. .. _v4.22.2: ------------------- 4.22.2 - 2019-05-07 ------------------- This patch fixes the very rare :issue:`1798` in :func:`~hypothesis.extra.numpy.array_dtypes`, which caused an internal error in our tests. .. _v4.22.1: ------------------- 4.22.1 - 2019-05-07 ------------------- This patch fixes a rare bug in :func:`from_type(range) `. Thanks to Zebulun Arendsee for fixing the bug at the PyCon 2019 Sprints. .. _v4.22.0: ------------------- 4.22.0 - 2019-05-07 ------------------- The ``unique_by`` argument to :obj:`~hypothesis.strategies.lists` now accepts a tuple of callables such that every element of the generated list will be unique with respect to each callable in the tuple (:issue:`1916`). Thanks to Marco Sirabella for this feature at the PyCon 2019 sprints! .. _v4.21.1: ------------------- 4.21.1 - 2019-05-06 ------------------- This patch cleans up the internals of :func:`~hypothesis.strategies.one_of`. You may see a slight change to the distribution of examples from this strategy but there is no change to the public API. Thanks to Marco Sirabella for writing this patch at the PyCon 2019 sprints! .. _v4.21.0: ------------------- 4.21.0 - 2019-05-05 ------------------- The :func:`~hypothesis.strategies.from_type` strategy now supports :class:`python:slice` objects. Thanks to Charlie El. Awbery for writing this feature at the `PyCon 2019 Mentored Sprints `__. .. _v4.20.0: ------------------- 4.20.0 - 2019-05-05 ------------------- This release improves the :func:`~hypothesis.extra.numpy.array_shapes` strategy, to choose an appropriate default for ``max_side`` based on the ``min_side``, and ``max_dims`` based on the ``min_dims``. An explicit error is raised for dimensions greater than 32, which are not supported by Numpy, as for other invalid combinations of arguments. Thanks to Jenny Rouleau for writing this feature at the `PyCon 2019 Mentored Sprints `__. .. _v4.19.0: ------------------- 4.19.0 - 2019-05-05 ------------------- The :func:`~hypothesis.strategies.from_type` strategy now supports :class:`python:range` objects (or ``xrange`` on Python 2). Thanks to Katrina Durance for writing this feature at the `PyCon 2019 Mentored Sprints `__. .. _v4.18.3: ------------------- 4.18.3 - 2019-04-30 ------------------- This release fixes a very rare edge case in the test-case mutator, which could cause an internal error with certain unusual tests. .. _v4.18.2: ------------------- 4.18.2 - 2019-04-30 ------------------- This patch makes Hypothesis compatible with the Python 3.8 alpha, which changed the representation of code objects to support positional-only arguments. Note however that Hypothesis does not (yet) support such functions as e.g. arguments to :func:`~hypothesis.strategies.builds` or inputs to :func:`@given `. Thanks to Paul Ganssle for identifying and fixing this bug. .. _v4.18.1: ------------------- 4.18.1 - 2019-04-29 ------------------- This patch improves the performance of unique collections such as :func:`~hypothesis.strategies.sets` when the elements are drawn from a :func:`~hypothesis.strategies.sampled_from` strategy (:issue:`1115`). .. _v4.18.0: ------------------- 4.18.0 - 2019-04-24 ------------------- This release adds the :func:`~hypothesis.strategies.functions` strategy, which can be used to imitate your 'real' function for callbacks. .. _v4.17.2: ------------------- 4.17.2 - 2019-04-19 ------------------- This release refactors stateful rule selection to share the new machinery with :func:`~hypothesis.strategies.sampled_from` instead of using the original independent implementation. .. _v4.17.1: ------------------- 4.17.1 - 2019-04-16 ------------------- This patch allows Hypothesis to try a few more examples after finding the first bug, in hopes of reporting multiple distinct bugs. The heuristics described in :issue:`847` ensure that we avoid wasting time on fruitless searches, while still surfacing each bug as soon as possible. .. _v4.17.0: ------------------- 4.17.0 - 2019-04-16 ------------------- This release adds the strategy :func:`~hypothesis.extra.numpy.broadcastable_shapes`, which generates array shapes that are `broadcast-compatible `_ with a provided shape. .. _v4.16.0: ------------------- 4.16.0 - 2019-04-12 ------------------- This release allows :func:`~hypothesis.strategies.register_type_strategy` to be used with :obj:`python:typing.NewType` instances. This may be useful to e.g. provide only positive integers for :func:`from_type(UserId) ` with a ``UserId = NewType('UserId', int)`` type. Thanks to PJCampi for suggesting and writing the patch! .. _v4.15.0: ------------------- 4.15.0 - 2019-04-09 ------------------- This release supports passing a :class:`~python:datetime.timedelta` as the :obj:`~hypothesis.settings.deadline` setting, so you no longer have to remember that the number is in milliseconds (:issue:`1900`). Thanks to Damon Francisco for this change! .. _v4.14.7: ------------------- 4.14.7 - 2019-04-09 ------------------- This patch makes the type annotations on ``hypothesis.extra.dateutil`` compatible with :pypi:`mypy` 0.700. .. _v4.14.6: ------------------- 4.14.6 - 2019-04-07 ------------------- This release fixes a bug introduced in :ref:`Hypothesis 4.14.3 ` that would sometimes cause :func:`sampled_from(...).filter(...) ` to encounter an internal assertion failure when there are three or fewer elements, and every element is rejected by the filter. .. _v4.14.5: ------------------- 4.14.5 - 2019-04-05 ------------------- This patch takes the previous efficiency improvements to :func:`sampled_from(...).filter(...) ` strategies that reject most elements, and generalises them to also apply to ``sampled_from(...).filter(...).filter(...)`` and longer chains of filters. .. _v4.14.4: ------------------- 4.14.4 - 2019-04-05 ------------------- This release fixes a bug that prevented :func:`~hypothesis.strategies.random_module` from correctly restoring the previous state of the ``random`` module. The random state was instead being restored to a temporary deterministic state, which accidentally caused subsequent tests to see the same random values across multiple test runs. .. _v4.14.3: ------------------- 4.14.3 - 2019-04-03 ------------------- This patch adds an internal special case to make :func:`sampled_from(...).filter(...) ` much more efficient when the filter rejects most elements (:issue:`1885`). .. _v4.14.2: ------------------- 4.14.2 - 2019-03-31 ------------------- This patch improves the error message if the function ``f`` in :func:`s.flatmap(f) ` does not return a strategy. Thanks to Kai Chen for this change! .. _v4.14.1: ------------------- 4.14.1 - 2019-03-30 ------------------- This release modifies how Hypothesis selects operations to run during shrinking, by causing it to deprioritise previously useless classes of shrink until others have reached a fixed point. This avoids certain pathological cases where the shrinker gets very close to finishing and then takes a very long time to finish the last small changes because it tries many useless shrinks for each useful one towards the end. It also should cause a more modest improvement (probably no more than about 30%) in shrinking performance for most tests. .. _v4.14.0: ------------------- 4.14.0 - 2019-03-19 ------------------- This release blocks installation of Hypothesis on Python 3.4, which :PEP:`reached its end of life date on 2019-03-18 <429>`. This should not be of interest to anyone but downstream maintainers - if you are affected, migrate to a secure version of Python as soon as possible or at least seek commercial support. .. _v4.13.0: ------------------- 4.13.0 - 2019-03-19 ------------------- This release makes it an explicit error to call :func:`floats(min_value=inf, exclude_min=True) ` or :func:`floats(max_value=-inf, exclude_max=True) `, as there are no possible values that can be generated (:issue:`1859`). :func:`floats(min_value=0.0, max_value=-0.0) ` is now deprecated. While ``0. == -0.`` and we could thus generate either if comparing by value, violating the sequence ordering of floats is a special case we don't want or need. .. _v4.12.1: ------------------- 4.12.1 - 2019-03-18 ------------------- This release should significantly reduce the amount of memory that Hypothesis uses for representing large test cases, by storing information in a more compact representation and only unpacking it lazily when it is first needed. .. _v4.12.0: ------------------- 4.12.0 - 2019-03-18 ------------------- This update adds the :obj:`~hypothesis.settings.report_multiple_bugs` setting, which you can use to disable multi-bug reporting and only raise whichever bug had the smallest minimal example. This is occasionally useful when using a debugger or tools that annotate tracebacks via introspection. .. _v4.11.7: ------------------- 4.11.7 - 2019-03-18 ------------------- This change makes a tiny improvement to the core engine's bookkeeping. There is no user-visible change. .. _v4.11.6: ------------------- 4.11.6 - 2019-03-15 ------------------- This release changes some of Hypothesis's internal shrinking behaviour in order to reduce memory usage and hopefully improve performance. .. _v4.11.5: ------------------- 4.11.5 - 2019-03-13 ------------------- This release adds a micro-optimisation to how Hypothesis handles debug reporting internally. Hard to shrink test may see a slight performance improvement, but in most common scenarios it is unlikely to be noticeable. .. _v4.11.4: ------------------- 4.11.4 - 2019-03-13 ------------------- This release removes some redundant code that was no longer needed but was still running a significant amount of computation and allocation on the hot path. This should result in a modest speed improvement for most tests, especially those with large test cases. .. _v4.11.3: ------------------- 4.11.3 - 2019-03-13 ------------------- This release adds a micro-optimisation to how Hypothesis caches test cases. This will cause a small improvement in speed and memory usage for large test cases, but in most common scenarios it is unlikely to be noticeable. .. _v4.11.2: ------------------- 4.11.2 - 2019-03-13 ------------------- This release removes some internal code that populates a field that is no longer used anywhere. This should result in some modest performance and speed improvements and no other user visible effects. .. _v4.11.1: ------------------- 4.11.1 - 2019-03-13 ------------------- This is a formatting-only patch, enabled by a new version of :pypi:`isort`. .. _v4.11.0: ------------------- 4.11.0 - 2019-03-12 ------------------- This release deprecates :func:`~hypothesis.strategies.sampled_from` with empty sequences. This returns :func:`~hypothesis.strategies.nothing`, which gives a clear error if used directly... but simply vanishes if combined with another strategy. Tests that silently generate less than expected are a serious problem for anyone relying on them to find bugs, and we think reliability more important than convenience in this case. .. _v4.10.0: ------------------- 4.10.0 - 2019-03-11 ------------------- This release improves Hypothesis's to detect flaky tests, by noticing when the behaviour of the test changes between runs. In particular this will notice many new cases where data generation depends on external state (e.g. external sources of randomness) and flag those as flaky sooner and more reliably. The basis of this feature is a considerable reengineering of how Hypothesis stores its history of test cases, so on top of this its memory usage should be considerably reduced. .. _v4.9.0: ------------------ 4.9.0 - 2019-03-09 ------------------ This release adds the strategy :func:`~hypothesis.extra.numpy.valid_tuple_axes`, which generates tuples of axis-indices that can be passed to the ``axis`` argument in NumPy's sequential functions (e.g. :func:`numpy:numpy.sum`). Thanks to Ryan Soklaski for this strategy. .. _v4.8.0: ------------------ 4.8.0 - 2019-03-06 ------------------ This release significantly tightens validation in :class:`hypothesis.settings`. :obj:`~hypothesis.settings.max_examples`, ``buffer_size``, and :obj:`~hypothesis.settings.stateful_step_count` must be positive integers; :obj:`~hypothesis.settings.deadline` must be a positive number or ``None``; and :obj:`~hypothesis.settings.derandomize` must be either ``True`` or ``False``. As usual, this replaces existing errors with a more helpful error and starts new validation checks as deprecation warnings. .. _v4.7.19: ------------------- 4.7.19 - 2019-03-04 ------------------- This release makes some micro-optimisations to certain calculations performed in the shrinker. These should particularly speed up large test cases where the shrinker makes many small changes. It will also reduce the amount allocated, but most of this is garbage that would have been immediately thrown away, so you probably won't see much effect specifically from that. .. _v4.7.18: ------------------- 4.7.18 - 2019-03-03 ------------------- This patch removes some overhead from :func:`~hypothesis.extra.numpy.arrays` with a constant shape and dtype. The resulting performance improvement is modest, but worthwhile for small arrays. .. _v4.7.17: ------------------- 4.7.17 - 2019-03-01 ------------------- This release makes some micro-optimisations within Hypothesis's internal representation of test cases. This should cause heavily nested test cases to allocate less during generation and shrinking, which should speed things up slightly. .. _v4.7.16: ------------------- 4.7.16 - 2019-02-28 ------------------- This changes the order in which Hypothesis runs certain operations during shrinking. This should significantly decrease memory usage and speed up shrinking of large examples. .. _v4.7.15: ------------------- 4.7.15 - 2019-02-28 ------------------- This release allows Hypothesis to calculate a number of attributes of generated test cases lazily. This should significantly reduce memory usage and modestly improve performance, especially for large test cases. .. _v4.7.14: ------------------- 4.7.14 - 2019-02-28 ------------------- This release reduces the number of operations the shrinker will try when reordering parts of a test case. This should in some circumstances significantly speed up shrinking. It *may* result in different final test cases, and if so usually slightly worse ones, but it should not generally have much impact on the end result as the operations removed were typically useless. .. _v4.7.13: ------------------- 4.7.13 - 2019-02-27 ------------------- This release changes how Hypothesis reorders examples within a test case during shrinking. This should make shrinking considerably faster. .. _v4.7.12: ------------------- 4.7.12 - 2019-02-27 ------------------- This release slightly improves the shrinker's ability to replace parts of a test case with their minimal version, by allowing it to do so in bulk rather than one at a time. Where this is effective, shrinker performance should be modestly improved. .. _v4.7.11: ------------------- 4.7.11 - 2019-02-25 ------------------- This release makes some micro-optimisations to common operations performed during shrinking. Shrinking should now be slightly faster, especially for large examples with relatively fast test functions. .. _v4.7.10: ------------------- 4.7.10 - 2019-02-25 ------------------- This release is a purely internal refactoring of Hypothesis's API for representing test cases. There should be no user visible effect. .. _v4.7.9: ------------------ 4.7.9 - 2019-02-24 ------------------ This release changes certain shrink passes to make them more efficient when they aren't making progress. .. _v4.7.8: ------------------ 4.7.8 - 2019-02-23 ------------------ This patch removes some unused code, which makes the internals a bit easier to understand. There is no user-visible impact. .. _v4.7.7: ------------------ 4.7.7 - 2019-02-23 ------------------ This release reduces the number of operations the shrinker will try when reordering parts of a test case. This should in some circumstances significantly speed up shrinking. It *may* result in different final test cases, and if so usually slightly worse ones, but it should not generally have much impact on the end result as the operations removed were typically useless. .. _v4.7.6: ------------------ 4.7.6 - 2019-02-23 ------------------ This patch removes some unused code from the shrinker. There is no user-visible change. .. _v4.7.5: ------------------ 4.7.5 - 2019-02-23 ------------------ This release changes certain shrink passes to make them *adaptive* - that is, in cases where they are successfully making progress they may now do so significantly faster. .. _v4.7.4: ------------------ 4.7.4 - 2019-02-22 ------------------ This is a docs-only patch, noting that because the :pypi:`lark-parser` is under active development at version 0.x, ``hypothesis[lark]`` APIs may break in minor releases if necessary to keep up with the upstream package. .. _v4.7.3: ------------------ 4.7.3 - 2019-02-22 ------------------ This changes Hypothesis to no longer import various test frameworks by default (if they are installed). which will speed up the initial ``import hypothesis`` call. .. _v4.7.2: ------------------ 4.7.2 - 2019-02-22 ------------------ This release changes Hypothesis's internal representation of a test case to calculate some expensive structural information on demand rather than eagerly. This should reduce memory usage a fair bit, and may make generation somewhat faster. .. _v4.7.1: ------------------ 4.7.1 - 2019-02-21 ------------------ This release refactors the internal representation of previously run test cases. The main thing you should see as a result is that Hypothesis becomes somewhat less memory hungry. .. _v4.7.0: ------------------ 4.7.0 - 2019-02-21 ------------------ This patch allows :func:`~hypothesis.extra.numpy.array_shapes` to generate shapes with side-length or even dimension zero, though the minimum still defaults to one. These shapes are rare and have some odd behavior, but are particularly important to test for just that reason! In a related bigfix, :func:`~hypothesis.extra.numpy.arrays` now supports generating zero-dimensional arrays with ``dtype=object`` and a strategy for iterable elements. Previously, the array element would incorrectly be set to the first item in the generated iterable. Thanks to Ryan Turner for continuing to improve our Numpy support. .. _v4.6.1: ------------------ 4.6.1 - 2019-02-19 ------------------ This release is a trivial micro-optimisation inside Hypothesis which should result in it using significantly less memory. .. _v4.6.0: ------------------ 4.6.0 - 2019-02-18 ------------------ This release changes some inconsistent behavior of :func:`~hypothesis.extra.numpy.arrays` from the Numpy extra when asked for an array of ``shape=()``. :func:`~hypothesis.extra.numpy.arrays` will now always return a Numpy :class:`~numpy:numpy.ndarray`, and the array will always be of the requested dtype. Thanks to Ryan Turner for this change. .. _v4.5.12: ------------------- 4.5.12 - 2019-02-18 ------------------- This release fixes a minor typo in an internal comment. There is no user-visible change. .. _v4.5.11: ------------------- 4.5.11 - 2019-02-15 ------------------- This release fixes :issue:`1813`, a bug introduced in :ref:`3.59.1 `, which caused :py:meth:`~hypothesis.strategies.random_module` to no longer affect the body of the test: Although Hypothesis would claim to be seeding the random module in fact tests would always run with a seed of zero. .. _v4.5.10: ------------------- 4.5.10 - 2019-02-14 ------------------- This patch fixes an off-by-one error in the maximum length of :func:`~hypothesis.strategies.emails`. Thanks to Krzysztof Jurewicz for :pull:`1812`. .. _v4.5.9: ------------------ 4.5.9 - 2019-02-14 ------------------ This patch removes some unused code from the shrinker. There is no user-visible change. .. _v4.5.8: ------------------ 4.5.8 - 2019-02-12 ------------------ This release fixes an internal ``IndexError`` in Hypothesis that could sometimes be triggered during shrinking. .. _v4.5.7: ------------------ 4.5.7 - 2019-02-11 ------------------ This release modifies the shrinker to interleave different types of reduction operations, e.g. switching between deleting data and lowering scalar values rather than trying entirely deletions then entirely lowering. This may slow things down somewhat in the typical case, but has the major advantage that many previously difficult to shrink examples should become much faster, because the shrinker will no longer tend to stall when trying some ineffective changes to the shrink target but will instead interleave it with other more effective operations. .. _v4.5.6: ------------------ 4.5.6 - 2019-02-11 ------------------ This release makes a number of internal changes to the implementation of :func:`hypothesis.extra.lark.from_lark`. These are primarily intended as a refactoring, but you may see some minor improvements to performance when generating large strings, and possibly to shrink quality. .. _v4.5.5: ------------------ 4.5.5 - 2019-02-10 ------------------ This patch prints an explanatory note when :issue:`1798` is triggered, because the error message from Numpy is too terse to locate the problem. .. _v4.5.4: ------------------ 4.5.4 - 2019-02-08 ------------------ In Python 2, ``long`` integers are not allowed in the shape argument to :func:`~hypothesis.extra.numpy.arrays`. Thanks to Ryan Turner for fixing this. .. _v4.5.3: ------------------ 4.5.3 - 2019-02-08 ------------------ This release makes a small internal refactoring to clarify how Hypothesis instructs tests to stop running when appropriate. There is no user-visible change. .. _v4.5.2: ------------------ 4.5.2 - 2019-02-06 ------------------ This release standardises all of the shrinker's internal operations on running in a random order. The main effect you will see from this that it should now be much less common for the shrinker to stall for a long time before making further progress. In some cases this will correspond to shrinking more slowly, but on average it should result in faster shrinking. .. _v4.5.1: ------------------ 4.5.1 - 2019-02-05 ------------------ This patch updates some docstrings, but has no runtime changes. .. _v4.5.0: ------------------ 4.5.0 - 2019-02-03 ------------------ This release adds ``exclude_min`` and ``exclude_max`` arguments to :func:`~hypothesis.strategies.floats`, so that you can easily generate values from :wikipedia:`open or half-open intervals ` (:issue:`1622`). .. _v4.4.6: ------------------ 4.4.6 - 2019-02-03 ------------------ This patch fixes a bug where :func:`~hypothesis.strategies.from_regex` could throw an internal error if the :obj:`python:re.IGNORECASE` flag was used (:issue:`1786`). .. _v4.4.5: ------------------ 4.4.5 - 2019-02-02 ------------------ This release removes two shrink passes that Hypothesis runs late in the process. These were very expensive when the test function was slow and often didn't do anything useful. Shrinking should get faster for most failing tests. If you see any regression in example quality as a result of this release, please let us know. .. _v4.4.4: ------------------ 4.4.4 - 2019-02-02 ------------------ This release modifies the way that Hypothesis deletes data during shrinking. It will primarily be noticeable for very large examples, which should now shrink faster. The shrinker is now also able to perform some deletions that it could not previously, but this is unlikely to be very noticeable. .. _v4.4.3: ------------------ 4.4.3 - 2019-01-25 ------------------ This release fixes an open file leak that used to cause ``ResourceWarning``\ s. .. _v4.4.2: ------------------ 4.4.2 - 2019-01-24 ------------------ This release changes Hypothesis's internal approach to caching the results of executing test cases. The result should be that it is now significantly less memory hungry, especially when shrinking large test cases. Some tests may get slower or faster depending on whether the new or old caching strategy was well suited to them, but any change in speed in either direction should be minor. .. _v4.4.1: ------------------ 4.4.1 - 2019-01-24 ------------------ This patch tightens up some of our internal heuristics to deal with shrinking floating point numbers, which will now run in fewer circumstances. You are fairly unlikely to see much difference from this, but if you do you are likely to see shrinking become slightly faster and/or producing slightly worse results. .. _v4.4.0: ------------------ 4.4.0 - 2019-01-24 ------------------ This release adds the :func:`~hypothesis.extra.django.from_form` function, which allows automatic testing against Django forms. (:issue:`35`) Thanks to Paul Stiverson for this feature, which resolves our oldest open issue! .. _v4.3.0: ------------------ 4.3.0 - 2019-01-24 ------------------ This release deprecates ``HealthCheck.hung_test`` and disables the associated runtime check for tests that ran for more than five minutes. Such a check is redundant now that we enforce the ``deadline`` and ``max_examples`` setting, which can be adjusted independently. .. _v4.2.0: ------------------ 4.2.0 - 2019-01-23 ------------------ This release adds a new module, ``hypothesis.extra.lark``, which you can use to generate strings matching a context-free grammar. In this initial version, only :pypi:`lark-parser` EBNF grammars are supported, by the new :func:`hypothesis.extra.lark.from_lark` function. .. _v4.1.2: ------------------ 4.1.2 - 2019-01-23 ------------------ This patch fixes a very rare overflow bug (:issue:`1748`) which could raise an ``InvalidArgument`` error in :func:`~hypothesis.strategies.complex_numbers` even though the arguments were valid. .. _v4.1.1: ------------------ 4.1.1 - 2019-01-23 ------------------ This release makes some improvements to internal code organisation and documentation and has no impact on behaviour. .. _v4.1.0: ------------------ 4.1.0 - 2019-01-22 ------------------ This release adds :func:`~hypothesis.register_random`, which registers ``random.Random`` instances or compatible objects to be seeded and reset by Hypothesis to ensure that test cases are deterministic. We still recommend explicitly passing a ``random.Random`` instance from :func:`~hypothesis.strategies.randoms` if possible, but registering a framework-global state for Hypothesis to manage is better than flaky tests! .. _v4.0.2: ------------------ 4.0.2 - 2019-01-22 ------------------ This patch fixes :issue:`1387`, where bounded :func:`~hypothesis.strategies.integers` with a very large range would almost always generate very large numbers. Now, we usually use the same tuned distribution as unbounded :func:`~hypothesis.strategies.integers`. .. _v4.0.1: ------------------ 4.0.1 - 2019-01-16 ------------------ This release randomizes the order in which the shrinker tries some of its initial normalization operations. You are unlikely to see much difference as a result unless your generated examples are very large. In this case you may see some performance improvements in shrinking. .. _v4.0.0: ------------------ 4.0.0 - 2019-01-14 ------------------ Welcome to the next major version of Hypothesis! There are no new features here, as we release those in minor versions. Instead, 4.0 is a chance for us to remove deprecated features (many already converted into no-ops), and turn a variety of warnings into errors. If you were running on the last version of Hypothesis 3.x *without any Hypothesis deprecation warnings* (or using private APIs), this will be a very boring upgrade. **In fact, nothing will change for you at all.** Per :ref:`our deprecation policy `, warnings added in the last six months (after 2018-07-05) have not been converted to errors. Removals ~~~~~~~~ - ``hypothesis.extra.datetime`` has been removed, replaced by the core date and time strategies. - ``hypothesis.extra.fakefactory`` has been removed, replaced by general expansion of Hypothesis' strategies and the third-party ecosystem. - The SQLite example database backend has been removed. Settings ~~~~~~~~ - The :obj:`~hypothesis.settings.deadline` is now enforced by default, rather than just emitting a warning when the default (200 milliseconds per test case) deadline is exceeded. - The ``database_file`` setting has been removed; use :obj:`~hypothesis.settings.database`. - The ``perform_health_check`` setting has been removed; use :obj:`~hypothesis.settings.suppress_health_check`. - The ``max_shrinks`` setting has been removed; use :obj:`~hypothesis.settings.phases` to disable shrinking. - The ``min_satisfying_examples``, ``max_iterations``, ``strict``, ``timeout``, and ``use_coverage`` settings have been removed without user-configurable replacements. Strategies ~~~~~~~~~~ - The ``elements`` argument is now required for collection strategies. - The ``average_size`` argument was a no-op and has been removed. - Date and time strategies now only accept ``min_value`` and ``max_value`` for bounds. - :func:`~hypothesis.strategies.builds` now requires that the thing to build is passed as the first positional argument. - Alphabet validation for :func:`~hypothesis.strategies.text` raises errors, not warnings, as does category validation for :func:`~hypothesis.strategies.characters`. - The ``choices()`` strategy has been removed. Instead, you can use :func:`~hypothesis.strategies.data` with :func:`~hypothesis.strategies.sampled_from`, so ``choice(elements)`` becomes ``data.draw(sampled_from(elements))``. - The ``streaming()`` strategy has been removed. Instead, you can use :func:`~hypothesis.strategies.data` and replace iterating over the stream with ``data.draw()`` calls. - :func:`~hypothesis.strategies.sampled_from` and :func:`~hypothesis.strategies.permutations` raise errors instead of warnings if passed a collection that is not a sequence. Miscellaneous ~~~~~~~~~~~~~ - Applying :func:`@given ` to a test function multiple times was really inefficient, and now it's also an error. - Using the ``.example()`` method of a strategy (intended for interactive exploration) within another strategy or a test function always weakened data generation and broke shrinking, and now it's an error too. - The ``HYPOTHESIS_DATABASE_FILE`` environment variable is no longer supported, as the ``database_file`` setting has been removed. - The ``HYPOTHESIS_VERBOSITY_LEVEL`` environment variable is no longer supported. You can use the ``--hypothesis-verbosity`` pytest argument instead, or write your own setup code using the settings profile system to replace it. - Using :func:`@seed ` or :obj:`derandomize=True ` now forces :obj:`database=None ` to ensure results are in fact reproducible. If :obj:`~hypothesis.settings.database` is *not* ``None``, doing so also emits a ``HypothesisWarning``. - Unused exception types have been removed from ``hypothesis.errors``; namely ``AbnormalExit``, ``BadData``, ``BadTemplateDraw``, ``DefinitelyNoSuchExample``, ``Timeout``, and ``WrongFormat``. Hypothesis 3.x ============== .. _v3.88.3: ------------------- 3.88.3 - 2019-01-11 ------------------- This changes the order that the shrinker tries certain operations in its "emergency" phase which runs late in the process. The new order should be better at avoiding long stalls where the shrinker is failing to make progress, which may be helpful if you have difficult to shrink test cases. However this will not be noticeable in the vast majority of use cases. .. _v3.88.2: ------------------- 3.88.2 - 2019-01-11 ------------------- This is a pure refactoring release that extracts some logic from the core Hypothesis engine into its own class and file. It should have no user visible impact. .. _v3.88.1: ------------------- 3.88.1 - 2019-01-11 ------------------- This patch fixes some markup in our documentation. .. _v3.88.0: ------------------- 3.88.0 - 2019-01-10 ------------------- Introduces :func:`hypothesis.stateful.multiple`, which allows rules in rule based state machines to send multiple results at once to their target Bundle, or none at all. .. _v3.87.0: ------------------- 3.87.0 - 2019-01-10 ------------------- This release contains a massive cleanup of the Hypothesis for Django extra: - ``hypothesis.extra.django.models.models()`` is deprecated in favor of :func:`hypothesis.extra.django.from_model`. - ``hypothesis.extra.django.models.add_default_field_mapping()`` is deprecated in favor of :func:`hypothesis.extra.django.register_field_strategy`. - :func:`~hypothesis.extra.django.from_model` does not infer a strategy for nullable fields or fields with a default unless passed ``infer``, like :func:`~hypothesis.strategies.builds`. ``models.models()`` would usually but not always infer, and a special ``default_value`` marker object was required to disable inference. .. _v3.86.9: ------------------- 3.86.9 - 2019-01-09 ------------------- This release improves some internal logic about when a test case in Hypothesis's internal representation could lead to a valid test case. In some circumstances this can lead to a significant speed up during shrinking. It may have some minor negative impact on the quality of the final result due to certain shrink passes now having access to less information about test cases in some circumstances, but this should rarely matter. .. _v3.86.8: ------------------- 3.86.8 - 2019-01-09 ------------------- This release has no user visible changes but updates our URLs to use HTTPS. .. _v3.86.7: ------------------- 3.86.7 - 2019-01-08 ------------------- Hypothesis can now automatically generate values for Django models with a `~django.db.models.URLField`, thanks to a new provisional strategy for URLs (:issue:`1388`). .. _v3.86.6: ------------------- 3.86.6 - 2019-01-07 ------------------- This release is a pure refactoring that extracts some internal code into its own file. It should have no user visible effect. .. _v3.86.5: ------------------- 3.86.5 - 2019-01-06 ------------------- This is a docs-only patch, which fixes some typos and removes a few hyperlinks for deprecated features. .. _v3.86.4: ------------------- 3.86.4 - 2019-01-04 ------------------- This release changes the order in which the shrinker tries to delete data. For large and slow tests this may significantly improve the performance of shrinking. .. _v3.86.3: ------------------- 3.86.3 - 2019-01-04 ------------------- This release fixes a bug where certain places Hypothesis internal errors could be raised during shrinking when a user exception occurred that suppressed an exception Hypothesis uses internally in its generation. The two known ways to trigger this problem were: * Errors raised in stateful tests' teardown function. * Errors raised in finally blocks that wrapped a call to ``data.draw``. These cases will now be handled correctly. .. _v3.86.2: ------------------- 3.86.2 - 2019-01-04 ------------------- This patch is a docs-only change to fix a broken hyperlink. .. _v3.86.1: ------------------- 3.86.1 - 2019-01-04 ------------------- This patch fixes :issue:`1732`, where :func:`~hypothesis.strategies.integers` would always return ``long`` values on Python 2. .. _v3.86.0: ------------------- 3.86.0 - 2019-01-03 ------------------- This release ensures that infinite numbers are never generated by :func:`~hypothesis.strategies.floats` with ``allow_infinity=False``, which could previously happen in some cases where one bound was also provided. The trivially inconsistent ``min_value=inf, allow_infinity=False`` now raises an InvalidArgumentError, as does the inverse with ``max_value``. You can still use :func:`just(inf) ` to generate ``inf`` without violating other constraints. .. _v3.85.3: ------------------- 3.85.3 - 2019-01-02 ------------------- Happy new year everyone! This release has no user visible changes but updates our copyright headers to include 2019. .. _v3.85.2: ------------------- 3.85.2 - 2018-12-31 ------------------- This release makes a small change to the way the shrinker works. You may see some improvements to speed of shrinking on especially large and hard to shrink examples, but most users are unlikely to see much difference. .. _v3.85.1: ------------------- 3.85.1 - 2018-12-30 ------------------- This patch fixes :issue:`1700`, where a line that contained a Unicode character before a lambda definition would cause an internal exception. .. _v3.85.0: ------------------- 3.85.0 - 2018-12-29 ------------------- Introduces the :func:`hypothesis.stateful.consumes` function. When defining a rule in stateful testing, it can be used to mark bundles from which values should be consumed, i. e. removed after use in the rule. This has been proposed in :issue:`136`. Thanks to Jochen Müller for this long-awaited feature. .. _v3.84.6: ------------------- 3.84.6 - 2018-12-28 ------------------- This patch makes a small internal change to fix an issue in Hypothesis's own coverage tests (:issue:`1718`). There is no user-visible change. .. _v3.84.5: ------------------- 3.84.5 - 2018-12-21 ------------------- This patch refactors the ``hypothesis.strategies`` module, so that private names should no longer appear in tab-completion lists. We previously relied on ``__all__`` for this, but not all editors respect it. .. _v3.84.4: ------------------- 3.84.4 - 2018-12-21 ------------------- This is a follow-up patch to ensure that the deprecation date is automatically recorded for any new deprecations. There is no user-visible effect. .. _v3.84.3: ------------------- 3.84.3 - 2018-12-20 ------------------- This patch updates the Hypothesis pytest plugin to avoid a recently deprecated hook interface. There is no user-visible change. .. _v3.84.2: ------------------- 3.84.2 - 2018-12-19 ------------------- This patch fixes the internals for :func:`~hypothesis.strategies.integers` with one bound. Values from this strategy now always shrink towards zero instead of towards the bound, and should shrink much more efficiently too. On Python 2, providing a bound incorrectly excluded ``long`` integers, which can now be generated. .. _v3.84.1: ------------------- 3.84.1 - 2018-12-18 ------------------- This patch adds information about when features were deprecated, but this is only recorded internally and has no user-visible effect. .. _v3.84.0: ------------------- 3.84.0 - 2018-12-18 ------------------- This release changes the stateful testing backend from ``find()`` to use :func:`@given ` (:issue:`1300`). This doesn't change how you create stateful tests, but does make them run more like other Hypothesis tests. :func:`@reproduce_failure ` and :func:`@seed ` now work for stateful tests. Stateful tests now respect the :attr:`~hypothesis.settings.deadline` and :attr:`~hypothesis.settings.suppress_health_check` settings, though they are disabled by default. You can enable them by using :func:`@settings(...) ` as a class decorator with whatever arguments you prefer. .. _v3.83.2: ------------------- 3.83.2 - 2018-12-17 ------------------- Hypothesis has adopted :pypi:`black` as our code formatter (:issue:`1686`). There are no functional changes to the source, but it's prettier! .. _v3.83.1: ------------------- 3.83.1 - 2018-12-13 ------------------- This patch increases the variety of examples generated by :func:`~hypothesis.strategies.from_type`. .. _v3.83.0: ------------------- 3.83.0 - 2018-12-12 ------------------- Our pytest plugin now warns you when strategy functions have been collected as tests, which may happen when e.g. using the :func:`@composite ` decorator when you should be using ``@given(st.data())`` for inline draws. Such functions *always* pass when treated as tests, because the lazy creation of strategies mean that the function body is never actually executed! .. _v3.82.6: ------------------- 3.82.6 - 2018-12-11 ------------------- Hypothesis can now :ref:`show statistics ` when running under :pypi:`pytest-xdist`. Previously, statistics were only reported when all tests were run in a single process (:issue:`700`). .. _v3.82.5: ------------------- 3.82.5 - 2018-12-08 ------------------- This patch fixes :issue:`1667`, where passing bounds of Numpy dtype ``int64`` to :func:`~hypothesis.strategies.integers` could cause errors on Python 3 due to internal rounding. .. _v3.82.4: ------------------- 3.82.4 - 2018-12-08 ------------------- Hypothesis now seeds and resets the global state of :mod:`np.random ` for each test case, to ensure that tests are reproducible. This matches and complements the existing handling of the :mod:`python:random` module - Numpy simply maintains an independent PRNG for performance reasons. .. _v3.82.3: ------------------- 3.82.3 - 2018-12-08 ------------------- This is a no-op release to add the new ``Framework :: Hypothesis`` `trove classifier `_ to :pypi:`hypothesis` on PyPI. You can `use it as a filter `_ to find Hypothesis-related packages such as extensions as they add the tag over the coming weeks, or simply visit :doc:`our curated list `. .. _v3.82.2: ------------------- 3.82.2 - 2018-12-08 ------------------- The :ref:`Hypothesis for Pandas extension ` is now listed in ``setup.py``, so you can ``pip install hypothesis[pandas]``. Thanks to jmshi for this contribution. .. _v3.82.1: ------------------- 3.82.1 - 2018-10-29 ------------------- This patch fixes :func:`~hypothesis.strategies.from_type` on Python 2 for classes where ``cls.__init__ is object.__init__``. Thanks to ccxcz for reporting :issue:`1656`. .. _v3.82.0: ------------------- 3.82.0 - 2018-10-29 ------------------- The ``alphabet`` argument for :func:`~hypothesis.strategies.text` now uses its default value of ``characters(exclude_categories=('Cs',))`` directly, instead of hiding that behind ``alphabet=None`` and replacing it within the function. Passing ``None`` is therefore deprecated. .. _v3.81.0: ------------------- 3.81.0 - 2018-10-27 ------------------- ``GenericStateMachine`` and :class:`~hypothesis.stateful.RuleBasedStateMachine` now raise an explicit error when instances of :obj:`~hypothesis.settings` are assigned to the classes' settings attribute, which is a no-op (:issue:`1643`). Instead assign to ``SomeStateMachine.TestCase.settings``, or use ``@settings(...)`` as a class decorator to handle this automatically. .. _v3.80.0: ------------------- 3.80.0 - 2018-10-25 ------------------- Since :ref:`version 3.68.0 `, :func:`~hypothesis.extra.numpy.arrays` checks that values drawn from the ``elements`` and ``fill`` strategies can be safely cast to the dtype of the array, and emits a warning otherwise. This release expands the checks to cover overflow for finite ``complex64`` elements and string truncation caused by too-long elements or trailing null characters (:issue:`1591`). .. _v3.79.4: ------------------- 3.79.4 - 2018-10-25 ------------------- Tests using :func:`@given ` now shrink errors raised from :pypi:`pytest` helper functions, instead of reporting the first example found. This was previously fixed in :ref:`version 3.56.0 `, but only for stateful testing. .. _v3.79.3: ------------------- 3.79.3 - 2018-10-23 ------------------- Traceback elision is now disabled on Python 2, to avoid an import-time :class:`python:SyntaxError` under Python < 2.7.9 (Python: :bpo:`21591`, :ref:`Hypothesis 3.79.2 `: :issue:`1648`). We encourage all users to `upgrade to Python 3 before the end of 2019 `_. .. _v3.79.2: ------------------- 3.79.2 - 2018-10-23 ------------------- This patch shortens tracebacks from Hypothesis, so you can see exactly happened in your code without having to skip over irrelevant details about our internals (:issue:`848`). In the example test (see :pull:`1582`), this reduces tracebacks from nine frames to just three - and for a test with multiple errors, from seven frames per error to just one! If you *do* want to see the internal details, you can disable frame elision by setting :obj:`~hypothesis.settings.verbosity` to ``debug``. .. _v3.79.1: ------------------- 3.79.1 - 2018-10-22 ------------------- The abstract number classes :class:`~python:numbers.Number`, :class:`~python:numbers.Complex`, :class:`~python:numbers.Real`, :class:`~python:numbers.Rational`, and :class:`~python:numbers.Integral` are now supported by the :func:`~hypothesis.strategies.from_type` strategy. Previously, you would have to use :func:`~hypothesis.strategies.register_type_strategy` before they could be resolved (:issue:`1636`) .. _v3.79.0: ------------------- 3.79.0 - 2018-10-18 ------------------- This release adds a CLI flag for verbosity ``--hypothesis-verbosity`` to the Hypothesis pytest plugin, applied after loading the profile specified by ``--hypothesis-profile``. Valid options are the names of verbosity settings, quiet, normal, verbose or debug. Thanks to Bex Dunn for writing this patch at the PyCon Australia sprints! The pytest header now correctly reports the current profile if ``--hypothesis-profile`` has been used. Thanks to Mathieu Paturel for the contribution at the Canberra Python Hacktoberfest. .. _v3.78.0: ------------------- 3.78.0 - 2018-10-16 ------------------- This release has deprecated the generation of integers, floats and fractions when the conversion of the upper and/ or lower bound is not 100% exact, e.g. when an integer gets passed a bound that is not a whole number. (:issue:`1625`) Thanks to Felix Grünewald for this patch during Hacktoberfest 2018. .. _v3.77.0: ------------------- 3.77.0 - 2018-10-16 ------------------- This minor release adds functionality to :obj:`~hypothesis.settings` allowing it to be used as a decorator on :obj:`~hypothesis.stateful.RuleBasedStateMachine` and ``GenericStateMachine``. Thanks to Tyler Nickerson for this feature in #hacktoberfest! .. _v3.76.1: ------------------- 3.76.1 - 2018-10-16 ------------------- This patch fixes some warnings added by recent releases of :pypi:`pydocstyle` and :pypi:`mypy`. .. _v3.76.0: ------------------- 3.76.0 - 2018-10-11 ------------------- This release deprecates using floats for ``min_size`` and ``max_size``. The type hint for ``average_size`` arguments has been changed from ``Optional[int]`` to None, because non-None values are always ignored and deprecated. .. _v3.75.4: ------------------- 3.75.4 - 2018-10-10 ------------------- This patch adds more internal comments to the core engine's sequence-length shrinker. There should be no user-visible change. .. _v3.75.3: ------------------- 3.75.3 - 2018-10-09 ------------------- This patch adds additional comments to some of the core engine's internal data structures. There is no user-visible change. .. _v3.75.2: ------------------- 3.75.2 - 2018-10-09 ------------------- This patch avoids caching a trivial case, fixing :issue:`493`. .. _v3.75.1: ------------------- 3.75.1 - 2018-10-09 ------------------- This patch fixes a broken link in a docstring. Thanks to Benjamin Lee for this contribution! .. _v3.75.0: ------------------- 3.75.0 - 2018-10-08 ------------------- This release deprecates the use of ``min_size=None``, setting the default ``min_size`` to 0 (:issue:`1618`). .. _v3.74.3: ------------------- 3.74.3 - 2018-10-08 ------------------- This patch makes some small internal changes to comply with a new lint setting in the build. There should be no user-visible change. .. _v3.74.2: ------------------- 3.74.2 - 2018-10-03 ------------------- This patch fixes :issue:`1153`, where time spent reifying a strategy was also counted in the time spent generating the first example. Strategies are now fully constructed and validated before the timer is started. .. _v3.74.1: ------------------- 3.74.1 - 2018-10-03 ------------------- This patch fixes some broken formatting and links in the documentation. .. _v3.74.0: ------------------- 3.74.0 - 2018-10-01 ------------------- This release checks that the value of the :attr:`~hypothesis.settings.print_blob` setting is a ``PrintSettings`` instance. Being able to specify a boolean value was not intended, and is now deprecated. In addition, specifying ``True`` will now cause the blob to always be printed, instead of causing it to be suppressed. Specifying any value that is not a ``PrintSettings`` or a boolean is now an error. .. _v3.73.5: ------------------- 3.73.5 - 2018-10-01 ------------------- Changes the documentation for ``hypothesis.strategies.datetimes``, ``hypothesis.strategies.dates``, ``hypothesis.strategies.times`` to use the new parameter names ``min_value`` and ``max_value`` instead of the deprecated names .. _v3.73.4: ------------------- 3.73.4 - 2018-09-30 ------------------- This patch ensures that Hypothesis deprecation warnings display the code that emitted them when you're not running in ``-Werror`` mode (:issue:`652`). .. _v3.73.3: ------------------- 3.73.3 - 2018-09-27 ------------------- Tracebacks involving :func:`@composite ` are now slightly shorter due to some internal refactoring. .. _v3.73.2: ------------------- 3.73.2 - 2018-09-26 ------------------- This patch fixes errors in the internal comments for one of the shrinker passes. There is no user-visible change. .. _v3.73.1: ------------------- 3.73.1 - 2018-09-25 ------------------- This patch substantially improves the distribution of data generated with :func:`~hypothesis.strategies.recursive`, and fixes a rare internal error (:issue:`1502`). .. _v3.73.0: ------------------- 3.73.0 - 2018-09-24 ------------------- This release adds the :func:`~hypothesis.extra.dpcontracts.fulfill` function, which is designed for testing code that uses :pypi:`dpcontracts` 0.4 or later for input validation. This provides some syntactic sugar around use of :func:`~hypothesis.assume`, to automatically filter out and retry calls that cause a precondition check to fail (:issue:`1474`). .. _v3.72.0: ------------------- 3.72.0 - 2018-09-24 ------------------- This release makes setting attributes of the :class:`hypothesis.settings` class an explicit error. This has never had any effect, but could mislead users who confused it with the current settings *instance* ``hypothesis.settings.default`` (which is also immutable). You can change the global settings with |settings.register_profile|. .. _v3.71.11: -------------------- 3.71.11 - 2018-09-24 -------------------- This patch factors out some common code in the shrinker for iterating over pairs of data blocks. There should be no user-visible change. .. _v3.71.10: -------------------- 3.71.10 - 2018-09-18 -------------------- This patch allows :func:`~hypothesis.strategies.from_type` to handle the empty tuple type, :obj:`typing.Tuple[()] `. .. _v3.71.9: ------------------- 3.71.9 - 2018-09-17 ------------------- This patch updates some internal comments for :pypi:`mypy`. There is no user-visible effect, even for Mypy users. .. _v3.71.8: ------------------- 3.71.8 - 2018-09-17 ------------------- This patch fixes a rare bug that would cause a particular shrinker pass to raise an IndexError, if a shrink improvement changed the underlying data in an unexpected way. .. _v3.71.7: ------------------- 3.71.7 - 2018-09-17 ------------------- This release fixes the broken cross-references in our docs, and adds a CI check so we don't add new ones. .. _v3.71.6: ------------------- 3.71.6 - 2018-09-16 ------------------- This patch fixes two bugs (:issue:`944` and :issue:`1521`), where messages about :func:`@seed ` did not check the current verbosity setting, and the wrong settings were active while executing explicit examples from |@example|. .. _v3.71.5: ------------------- 3.71.5 - 2018-09-15 ------------------- This patch fixes a ``DeprecationWarning`` added in Python 3.8 (:issue:`1576`). Thanks to tirkarthi for this contribution! .. _v3.71.4: ------------------- 3.71.4 - 2018-09-14 ------------------- This is a no-op release, which implements automatic DOI minting and code archival of Hypothesis via `Zenodo `_. Thanks to CERN and the EU *Horizon 2020* programme for providing this service! Check our :gh-file:`CITATION.cff` file for details, or head right on over to `doi.org/10.5281/zenodo.1412597 `_ .. _v3.71.3: ------------------- 3.71.3 - 2018-09-10 ------------------- This release adds the test name to some deprecation warnings, for easier debugging. Thanks to Sanyam Khurana for the patch! .. _v3.71.2: ------------------- 3.71.2 - 2018-09-10 ------------------- This release makes Hypothesis's memory usage substantially smaller for tests with many examples, by bounding the number of past examples it keeps around. You will not see much difference unless you are running tests with :obj:`~hypothesis.settings.max_examples` set to well over ``1000``, but if you do have such tests then you should see memory usage mostly plateau where previously it would have grown linearly with time. .. _v3.71.1: ------------------- 3.71.1 - 2018-09-09 ------------------- This patch adds internal comments to some tree traversals in the core engine. There is no user-visible change. .. _v3.71.0: ------------------- 3.71.0 - 2018-09-08 ------------------- This release deprecates the coverage-guided testing functionality, as it has proven brittle and does not really pull its weight. We intend to replace it with something more useful in the future, but the feature in its current form does not seem to be worth the cost of using, and whatever replaces it will likely look very different. .. _v3.70.4: ------------------- 3.70.4 - 2018-09-08 ------------------- This patch changes the behaviour of :func:`~hypothesis.reproduce_failure` so that blobs are only printed in quiet mode when the :obj:`~hypothesis.settings.print_blob` setting is set to ``ALWAYS``. Thanks to Cameron McGill for writing this patch at the PyCon Australia sprints! .. _v3.70.3: ------------------- 3.70.3 - 2018-09-03 ------------------- This patch removes some unnecessary code from the internals. There is no user-visible change. .. _v3.70.2: ------------------- 3.70.2 - 2018-09-03 ------------------- This patch fixes an internal bug where a corrupted argument to :func:`@reproduce_failure ` could raise the wrong type of error. Thanks again to Paweł T. Jochym, who maintains Hypothesis on `conda-forge `_ and consistently provides excellent bug reports including :issue:`1558`. .. _v3.70.1: ------------------- 3.70.1 - 2018-09-03 ------------------- This patch updates hypothesis to report its version and settings when run with pytest. (:issue:`1223`). Thanks to Jack Massey for this feature. .. _v3.70.0: ------------------- 3.70.0 - 2018-09-01 ------------------- This release adds a ``fullmatch`` argument to :func:`~hypothesis.strategies.from_regex`. When ``fullmatch=True``, the whole example will match the regex pattern as for :func:`python:re.fullmatch`. Thanks to Jakub Nabaglo for writing this patch at the PyCon Australia sprints! .. _v3.69.12: -------------------- 3.69.12 - 2018-08-30 -------------------- This release reverts the changes to logging handling in 3.69.11, which broke test that use the :pypi:`pytest` ``caplog`` fixture internally because all logging was disabled (:issue:`1546`). .. _v3.69.11: -------------------- 3.69.11 - 2018-08-29 -------------------- This patch will hide all logging messages produced by test cases before the final, minimal, failing test case (:issue:`356`). Thanks to Gary Donovan for writing this patch at the PyCon Australia sprints! .. _v3.69.10: -------------------- 3.69.10 - 2018-08-29 -------------------- This patch fixes a bug that prevents coverage from reporting unexecuted Python files (:issue:`1085`). Thanks to Gary Donovan for writing this patch at the PyCon Australia sprints! .. _v3.69.9: ------------------- 3.69.9 - 2018-08-28 ------------------- This patch improves the packaging of the Python package by adding ``LICENSE.txt`` to the sdist (:issue:`1311`), clarifying the minimum supported versions of :pypi:`pytz` and :pypi:`dateutil ` (:issue:`1383`), and adds keywords to the metadata (:issue:`1520`). Thanks to Graham Williamson for writing this patch at the PyCon Australia sprints! .. _v3.69.8: ------------------- 3.69.8 - 2018-08-28 ------------------- This is an internal change which replaces pickle with json to prevent possible security issues. Thanks to Vidya Rani D G for writing this patch at the PyCon Australia sprints! .. _v3.69.7: ------------------- 3.69.7 - 2018-08-28 ------------------- This patch ensures that :func:`~hypothesis.note` prints the note for every test case when the :obj:`~hypothesis.settings.verbosity` setting is ``Verbosity.verbose``. At normal verbosity it only prints from the final test case. Thanks to Tom McDermott for writing this patch at the PyCon Australia sprints! .. _v3.69.6: ------------------- 3.69.6 - 2018-08-27 ------------------- This patch improves the testing of some internal caching. It should have no user-visible effect. .. _v3.69.5: ------------------- 3.69.5 - 2018-08-27 ------------------- This change performs a small rename and refactoring in the core engine. There is no user-visible change. .. _v3.69.4: ------------------- 3.69.4 - 2018-08-27 ------------------- This change improves the core engine's ability to avoid unnecessary work, by consulting its cache of previously-tried inputs in more cases. .. _v3.69.3: ------------------- 3.69.3 - 2018-08-27 ------------------- This patch handles passing an empty :class:`python:enum.Enum` to :func:`~hypothesis.strategies.from_type` by returning :func:`~hypothesis.strategies.nothing`, instead of raising an internal :class:`python:AssertionError`. Thanks to Paul Amazona for writing this patch at the PyCon Australia sprints! .. _v3.69.2: ------------------- 3.69.2 - 2018-08-23 ------------------- This patch fixes a small mistake in an internal comment. There is no user-visible change. .. _v3.69.1: ------------------- 3.69.1 - 2018-08-21 ------------------- This change fixes a small bug in how the core engine consults its cache of previously-tried inputs. There is unlikely to be any user-visible change. .. _v3.69.0: ------------------- 3.69.0 - 2018-08-20 ------------------- This release improves argument validation for stateful testing. - If the target or targets of a :func:`~hypothesis.stateful.rule` are invalid, we now raise a useful validation error rather than an internal exception. - Passing both the ``target`` and ``targets`` arguments is deprecated - append the ``target`` bundle to the ``targets`` tuple of bundles instead. - Passing the name of a Bundle rather than the Bundle itself is also deprecated. .. _v3.68.3: ------------------- 3.68.3 - 2018-08-20 ------------------- This is a docs-only patch, fixing some typos and formatting issues. .. _v3.68.2: ------------------- 3.68.2 - 2018-08-19 ------------------- This change fixes a small bug in how the core engine caches the results of previously-tried inputs. The effect is unlikely to be noticeable, but it might avoid unnecessary work in some cases. .. _v3.68.1: ------------------- 3.68.1 - 2018-08-18 ------------------- This patch documents the :func:`~hypothesis.extra.numpy.from_dtype` function, which infers a strategy for :class:`numpy:numpy.dtype`\ s. This is used in :func:`~hypothesis.extra.numpy.arrays`, but can also be used directly when creating e.g. Pandas objects. .. _v3.68.0: ------------------- 3.68.0 - 2018-08-15 ------------------- :func:`~hypothesis.extra.numpy.arrays` now checks that integer and float values drawn from ``elements`` and ``fill`` strategies can be safely cast to the dtype of the array, and emits a warning otherwise (:issue:`1385`). Elements in the resulting array could previously violate constraints on the elements strategy due to floating-point overflow or truncation of integers to fit smaller types. .. _v3.67.1: ------------------- 3.67.1 - 2018-08-14 ------------------- This release contains a tiny refactoring of the internals. There is no user-visible change. .. _v3.67.0: ------------------- 3.67.0 - 2018-08-10 ------------------- This release adds a ``width`` argument to :func:`~hypothesis.strategies.floats`, to generate lower-precision floating point numbers for e.g. Numpy arrays. The generated examples are always instances of Python's native ``float`` type, which is 64bit, but passing ``width=32`` will ensure that all values can be exactly represented as 32bit floats. This can be useful to avoid overflow (to +/- infinity), and for efficiency of generation and shrinking. Half-precision floats (``width=16``) are also supported, but require Numpy if you are running Python 3.5 or earlier. .. _v3.66.33: -------------------- 3.66.33 - 2018-08-10 -------------------- This release fixes a bug in :func:`~hypothesis.strategies.floats`, where setting ``allow_infinity=False`` and exactly one of ``min_value`` and ``max_value`` would allow infinite values to be generated. .. _v3.66.32: -------------------- 3.66.32 - 2018-08-09 -------------------- This release adds type hints to the :obj:`@example() ` and :func:`~hypothesis.seed` decorators, and fixes the type hint on :func:`~hypothesis.strategies.register_type_strategy`. The second argument to :func:`~hypothesis.strategies.register_type_strategy` must either be a ``SearchStrategy``, or a callable which takes a ``type`` and returns a ``SearchStrategy``. .. _v3.66.31: -------------------- 3.66.31 - 2018-08-08 -------------------- Another set of changes designed to improve the performance of shrinking on large examples. In particular the shrinker should now spend considerably less time running useless shrinks. .. _v3.66.30: -------------------- 3.66.30 - 2018-08-06 -------------------- "Bug fixes and performance improvements". This release is a fairly major overhaul of the shrinker designed to improve its behaviour on large examples, especially around stateful testing. You should hopefully see shrinking become much faster, with little to no quality degradation (in some cases quality may even improve). .. _v3.66.29: -------------------- 3.66.29 - 2018-08-05 -------------------- This release fixes two very minor bugs in the core engine: * it fixes a corner case that was missing in :ref:`3.66.28 `, which should cause shrinking to work slightly better. * it fixes some logic for how shrinking interacts with the database that was causing Hypothesis to be insufficiently aggressive about clearing out old keys. .. _v3.66.28: -------------------- 3.66.28 - 2018-08-05 -------------------- This release improves how Hypothesis handles reducing the size of integers' representation. This change should mostly be invisible as it's purely about the underlying representation and not the generated value, but it may result in some improvements to shrink performance. .. _v3.66.27: -------------------- 3.66.27 - 2018-08-05 -------------------- This release changes the order in which Hypothesis chooses parts of the test case to shrink. For typical usage this should be a significant performance improvement on large examples. It is unlikely to have a major impact on example quality, but where it does change the result it should usually be an improvement. .. _v3.66.26: -------------------- 3.66.26 - 2018-08-05 -------------------- This release improves the debugging information that the shrinker emits about the operations it performs, giving better summary statistics about which passes resulted in test executions and whether they were successful. .. _v3.66.25: -------------------- 3.66.25 - 2018-08-05 -------------------- This release fixes several bugs that were introduced to the shrinker in :ref:`3.66.24 ` which would have caused it to behave significantly less well than advertised. With any luck you should *actually* see the promised benefits now. .. _v3.66.24: -------------------- 3.66.24 - 2018-08-03 -------------------- This release changes how Hypothesis deletes data when shrinking in order to better handle deletion of large numbers of contiguous sequences. Most tests should see little change, but this will hopefully provide a significant speed up for :ref:`stateful testing `. .. _v3.66.23: -------------------- 3.66.23 - 2018-08-02 -------------------- This release makes some internal changes to enable further improvements to the shrinker. You may see some changes in the final shrunk examples, but they are unlikely to be significant. .. _v3.66.22: -------------------- 3.66.22 - 2018-08-01 -------------------- This release adds some more internal caching to the shrinker. This should cause a significant speed up for shrinking, especially for stateful testing and large example sizes. .. _v3.66.21: -------------------- 3.66.21 - 2018-08-01 -------------------- This patch is for downstream packagers - our tests now pass under :pypi:`pytest` 3.7.0 (released 2018-07-30). There are no changes to the source of Hypothesis itself. .. _v3.66.20: -------------------- 3.66.20 - 2018-08-01 -------------------- This release removes some functionality from the shrinker that was taking a considerable amount of time and does not appear to be useful any more due to a number of quality improvements in the shrinker. You may see some degradation in shrink quality as a result of this, but mostly shrinking should just get much faster. .. _v3.66.19: -------------------- 3.66.19 - 2018-08-01 -------------------- This release slightly changes the format of some debugging information emitted during shrinking, and refactors some of the internal interfaces around that. .. _v3.66.18: -------------------- 3.66.18 - 2018-07-31 -------------------- This release is a very small internal refactoring which should have no user visible impact. .. _v3.66.17: -------------------- 3.66.17 - 2018-07-31 -------------------- This release fixes a bug that could cause an ``IndexError`` to be raised from inside Hypothesis during shrinking. It is likely that it was impossible to trigger this bug in practice - it was only made visible by some currently unreleased work. .. _v3.66.16: -------------------- 3.66.16 - 2018-07-31 -------------------- This release is a very small internal refactoring which should have no user visible impact. .. _v3.66.15: -------------------- 3.66.15 - 2018-07-31 -------------------- This release makes Hypothesis's shrinking faster by removing some redundant work that it does when minimizing values in its internal representation. .. _v3.66.14: -------------------- 3.66.14 - 2018-07-30 -------------------- This release expands the deprecation of timeout from :ref:`3.16.0 ` to also emit the deprecation warning in ``find`` or :ref:`stateful testing `. .. _v3.66.13: -------------------- 3.66.13 - 2018-07-30 -------------------- This release adds an additional shrink pass that is able to reduce the size of examples in some cases where the transformation is non-obvious. In particular this will improve the quality of some examples which would have regressed in :ref:`3.66.12 `. .. _v3.66.12: -------------------- 3.66.12 - 2018-07-28 -------------------- This release changes how we group data together for shrinking. It should result in improved shrinker performance, especially in stateful testing. .. _v3.66.11: -------------------- 3.66.11 - 2018-07-28 -------------------- This patch modifies how which rule to run is selected during :ref:`rule based stateful testing `. This should result in a slight performance increase during generation and a significant performance and quality improvement when shrinking. As a result of this change, some state machines which would previously have thrown an ``InvalidDefinition`` are no longer detected as invalid. .. _v3.66.10: -------------------- 3.66.10 - 2018-07-28 -------------------- This release weakens some minor functionality in the shrinker that had only modest benefit and made its behaviour much harder to reason about. This is unlikely to have much user visible effect, but it is possible that in some cases shrinking may get slightly slower. It is primarily to make it easier to work on the shrinker and pave the way for future work. .. _v3.66.9: ------------------- 3.66.9 - 2018-07-26 ------------------- This release improves the information that Hypothesis emits about its shrinking when :obj:`~hypothesis.settings.verbosity` is set to debug. .. _v3.66.8: ------------------- 3.66.8 - 2018-07-24 ------------------- This patch includes some minor fixes in the documentation, and updates the minimum version of :pypi:`pytest` to 3.0 (released August 2016). .. _v3.66.7: ------------------- 3.66.7 - 2018-07-24 ------------------- This release fixes a bug where difficult to shrink tests could sometimes trigger an internal assertion error inside the shrinker. .. _v3.66.6: ------------------- 3.66.6 - 2018-07-23 ------------------- This patch ensures that Hypothesis fully supports Python 3.7, by upgrading :func:`~hypothesis.strategies.from_type` (:issue:`1264`) and fixing some minor issues in our test suite (:issue:`1148`). .. _v3.66.5: ------------------- 3.66.5 - 2018-07-22 ------------------- This patch fixes the online docs for various extras, by ensuring that their dependencies are installed on readthedocs.io (:issue:`1326`). .. _v3.66.4: ------------------- 3.66.4 - 2018-07-20 ------------------- This release improves the shrinker's ability to reorder examples. For example, consider the following test: .. code-block:: python import hypothesis.strategies as st from hypothesis import given @given(st.text(), st.text()) def test_non_equal(x, y): assert x != y Previously this could have failed with either of ``x="", y="0"`` or ``x="0", y=""``. Now it should always fail with ``x="", y="0"``. This will allow the shrinker to produce more consistent results, especially in cases where test cases contain some ordered collection whose actual order does not matter. .. _v3.66.3: ------------------- 3.66.3 - 2018-07-20 ------------------- This patch fixes inference in the :func:`~hypothesis.strategies.builds` strategy with subtypes of :obj:`python:typing.NamedTuple`, where the ``__init__`` method is not useful for introspection. We now use the field types instead - thanks to James Uther for identifying this bug. .. _v3.66.2: ------------------- 3.66.2 - 2018-07-19 ------------------- This release improves the shrinker's ability to handle situations where there is an additive constraint between two values. For example, consider the following test: .. code-block:: python import hypothesis.strategies as st from hypothesis import given @given(st.integers(), st.integers()) def test_does_not_exceed_100(m, n): assert m + n < 100 Previously this could have failed with almost any pair ``(m, n)`` with ``0 <= m <= n`` and ``m + n == 100``. Now it should almost always fail with ``m=0, n=100``. This is a relatively niche specialisation, but can be useful in situations where e.g. a bug is triggered by an integer overflow. .. _v3.66.1: ------------------- 3.66.1 - 2018-07-09 ------------------- This patch fixes a rare bug where an incorrect percentage drawtime could be displayed for a test, when the system clock was changed during a test running under Python 2 (we use :func:`python:time.monotonic` where it is available to avoid such problems). It also fixes a possible zero-division error that can occur when the underlying C library double-rounds an intermediate value in :func:`python:math.fsum` and gets the least significant bit wrong. .. _v3.66.0: ------------------- 3.66.0 - 2018-07-05 ------------------- This release improves validation of the ``alphabet`` argument to the :func:`~hypothesis.strategies.text` strategy. The following misuses are now deprecated, and will be an error in a future version: - passing an unordered collection (such as ``set('abc')``), which violates invariants about shrinking and reproducibility - passing an alphabet sequence with elements that are not strings - passing an alphabet sequence with elements that are not of length one, which violates any size constraints that may apply Thanks to Sushobhit for adding these warnings (:issue:`1329`). .. _v3.65.3: ------------------- 3.65.3 - 2018-07-04 ------------------- This release fixes a mostly theoretical bug where certain usage of the internal API could trigger an assertion error inside Hypothesis. It is unlikely that this problem is even possible to trigger through the public API. .. _v3.65.2: ------------------- 3.65.2 - 2018-07-04 ------------------- This release fixes dependency information for coverage. Previously Hypothesis would allow installing :pypi:`coverage` with any version, but it only works with coverage 4.0 or later. We now specify the correct metadata in our ``setup.py``, so Hypothesis will only allow installation with compatible versions of coverage. .. _v3.65.1: ------------------- 3.65.1 - 2018-07-03 ------------------- This patch ensures that :ref:`stateful tests ` which raise an error from a :pypi:`pytest` helper still print the sequence of steps taken to reach that point (:issue:`1372`). This reporting was previously broken because the helpers inherit directly from :class:`python:BaseException`, and therefore require special handling to catch without breaking e.g. the use of ctrl-C to quit the test. .. _v3.65.0: ------------------- 3.65.0 - 2018-06-30 ------------------- This release deprecates the ``max_shrinks`` setting in favor of an internal heuristic. If you need to avoid shrinking examples, use the :obj:`~hypothesis.settings.phases` setting instead. (:issue:`1235`) .. _v3.64.2: ------------------- 3.64.2 - 2018-06-27 ------------------- This release fixes a bug where an internal assertion error could sometimes be triggered while shrinking a failing test. .. _v3.64.1: ------------------- 3.64.1 - 2018-06-27 ------------------- This patch fixes type-checking errors in our vendored pretty-printer, which were ignored by our mypy config but visible for anyone else (whoops). Thanks to Pi Delport for reporting :issue:`1359` so promptly. .. _v3.64.0: ------------------- 3.64.0 - 2018-06-26 ------------------- This release adds :ref:`an interface ` which can be used to insert a wrapper between the original test function and :func:`@given ` (:issue:`1257`). This will be particularly useful for test runner extensions such as :pypi:`pytest-trio`, but is not recommended for direct use by other users of Hypothesis. .. _v3.63.0: ------------------- 3.63.0 - 2018-06-26 ------------------- This release adds a new mechanism to infer strategies for classes defined using :pypi:`attrs`, based on the type, converter, or validator of each attribute. This inference is now built in to :func:`~hypothesis.strategies.builds` and :func:`~hypothesis.strategies.from_type`. On Python 2, :func:`~hypothesis.strategies.from_type` no longer generates instances of ``int`` when passed ``long``, or vice-versa. .. _v3.62.0: ------------------- 3.62.0 - 2018-06-26 ------------------- This release adds :PEP:`484` type hints to Hypothesis on a provisional basis, using the comment-based syntax for Python 2 compatibility. It *also* adds the ``py.typed`` marker specified in :PEP:`561`. After you ``pip install hypothesis``, :pypi:`mypy` 0.590 or later will therefore type-check your use of our public interface! .. _v3.61.0: ------------------- 3.61.0 - 2018-06-24 ------------------- This release deprecates the use of :class:`~hypothesis.settings` as a context manager, the use of which is somewhat ambiguous. Users should define settings with global state or with the :func:`@settings(...) ` decorator. .. _v3.60.1: ------------------- 3.60.1 - 2018-06-20 ------------------- Fixed a bug in generating an instance of a Django model from a strategy where the primary key is generated as part of the strategy. See :ref:`details here `. Thanks to Tim Martin for this contribution. .. _v3.60.0: ------------------- 3.60.0 - 2018-06-20 ------------------- This release adds the :func:`@initialize ` decorator for stateful testing (originally discussed in :issue:`1216`). All :func:`@initialize ` rules will be called once each in an arbitrary order before any normal rule is called. .. _v3.59.3: ------------------- 3.59.3 - 2018-06-19 ------------------- This is a no-op release to take into account some changes to the release process. It should have no user visible effect. .. _v3.59.2: ------------------- 3.59.2 - 2018-06-18 ------------------- This adds support for partially sorting examples which cannot be fully sorted. For example, [5, 4, 3, 2, 1, 0] with a constraint that the first element needs to be larger than the last becomes [1, 2, 3, 4, 5, 0]. Thanks to Luke for contributing. .. _v3.59.1: ------------------- 3.59.1 - 2018-06-16 ------------------- This patch uses :func:`python:random.getstate` and :func:`python:random.setstate` to restore the PRNG state after :func:`@given ` runs deterministic tests. Without restoring state, you might have noticed problems such as :issue:`1266`. The fix also applies to stateful testing (:issue:`702`). .. _v3.59.0: ------------------- 3.59.0 - 2018-06-14 ------------------- This release adds the :func:`~hypothesis.strategies.emails` strategy, which generates unicode strings representing an email address. Thanks to Sushobhit for moving this to the public API (:issue:`162`). .. _v3.58.1: ------------------- 3.58.1 - 2018-06-13 ------------------- This improves the shrinker. It can now reorder examples: 3 1 2 becomes 1 2 3. Thanks to Luke for contributing. .. _v3.58.0: ------------------- 3.58.0 - 2018-06-13 ------------------- This adds a new extra :py:func:`~hypothesis.extra.dateutil.timezones` strategy that generates :pypi:`dateutil timezones `. Thanks to Conrad for contributing. .. _v3.57.0: ------------------- 3.57.0 - 2018-05-20 ------------------- Using an unordered collection with the :func:`~hypothesis.strategies.permutations` strategy has been deprecated because the order in which e.g. a set shrinks is arbitrary. This may cause different results between runs. .. _v3.56.10: -------------------- 3.56.10 - 2018-05-16 -------------------- This release makes ``hypothesis.settings.define_setting`` a private method, which has the effect of hiding it from the documentation. .. _v3.56.9: ------------------- 3.56.9 - 2018-05-11 ------------------- This is another release with no functionality changes as part of changes to Hypothesis's new release tagging scheme. .. _v3.56.8: ------------------- 3.56.8 - 2018-05-10 ------------------- This is a release with no functionality changes that moves Hypothesis over to a new release tagging scheme. .. _v3.56.7: ------------------- 3.56.7 - 2018-05-10 ------------------- This release provides a performance improvement for most tests, but in particular users of :func:`~hypothesis.strategies.sampled_from` who don't have numpy installed should see a significant performance improvement. .. _v3.56.6: ------------------- 3.56.6 - 2018-05-09 ------------------- This patch contains further internal work to support Mypy. There are no user-visible changes... yet. .. _v3.56.5: ------------------- 3.56.5 - 2018-04-22 ------------------- This patch contains some internal refactoring to run :pypi:`mypy` in CI. There are no user-visible changes. .. _v3.56.4: ------------------- 3.56.4 - 2018-04-21 ------------------- This release involves some very minor internal clean up and should have no user visible effect at all. .. _v3.56.3: ------------------- 3.56.3 - 2018-04-20 ------------------- This release fixes a problem introduced in :ref:`3.56.0 ` where setting the hypothesis home directory (through currently undocumented means) would no longer result in the default database location living in the new home directory. .. _v3.56.2: ------------------- 3.56.2 - 2018-04-20 ------------------- This release fixes a problem introduced in :ref:`3.56.0 ` where setting :obj:`~hypothesis.settings.max_examples` to ``1`` would result in tests failing with ``Unsatisfiable``. This problem could also occur in other harder to trigger circumstances (e.g. by setting it to a low value, having a hard to satisfy assumption, and disabling health checks). .. _v3.56.1: ------------------- 3.56.1 - 2018-04-20 ------------------- This release fixes a problem that was introduced in :ref:`3.56.0 `: Use of the ``HYPOTHESIS_VERBOSITY_LEVEL`` environment variable was, rather than deprecated, actually broken due to being read before various setup the deprecation path needed was done. It now works correctly (and emits a deprecation warning). .. _v3.56.0: ------------------- 3.56.0 - 2018-04-17 ------------------- This release deprecates several redundant or internally oriented :class:`~hypothesis.settings`, working towards an orthogonal set of configuration options that are widely useful *without* requiring any knowledge of our internals (:issue:`535`). - Deprecated settings that no longer have any effect are no longer shown in the ``__repr__`` unless set to a non-default value. - ``hypothesis.settings.perform_health_check`` is deprecated, as it duplicates :obj:`~hypothesis.settings.suppress_health_check`. - ``hypothesis.settings.max_iterations`` is deprecated and disabled, because we can usually get better behaviour from an internal heuristic than a user-controlled setting. - ``hypothesis.settings.min_satisfying_examples`` is deprecated and disabled, due to overlap with the :obj:`~hypothesis.HealthCheck.filter_too_much` healthcheck and poor interaction with :obj:`~hypothesis.settings.max_examples`. - ``HYPOTHESIS_VERBOSITY_LEVEL`` is now deprecated. Set :obj:`~hypothesis.settings.verbosity` through the profile system instead. - Examples tried by ``find()`` are now reported at ``debug`` verbosity level (as well as ``verbose`` level). .. _v3.55.6: ------------------- 3.55.6 - 2018-04-14 ------------------- This release fixes a somewhat obscure condition (:issue:`1230`) under which you could occasionally see a failing test trigger an assertion error inside Hypothesis instead of failing normally. .. _v3.55.5: ------------------- 3.55.5 - 2018-04-14 ------------------- This patch fixes one possible cause of :issue:`966`. When running Python 2 with hash randomisation, passing a :obj:`python:bytes` object to :func:`python:random.seed` would use ``version=1``, which broke :obj:`~hypothesis.settings.derandomize` (because the seed depended on a randomised hash). If :obj:`~hypothesis.settings.derandomize` is *still* nondeterministic for you, please open an issue. .. _v3.55.4: ------------------- 3.55.4 - 2018-04-13 ------------------- This patch makes a variety of minor improvements to the documentation, and improves a few validation messages for invalid inputs. .. _v3.55.3: ------------------- 3.55.3 - 2018-04-12 ------------------- This release updates the URL metadata associated with the PyPI package (again). It has no other user visible effects. .. _v3.55.2: ------------------- 3.55.2 - 2018-04-11 ------------------- This release updates the URL metadata associated with the PyPI package. It has no other user visible effects. .. _v3.55.1: ------------------- 3.55.1 - 2018-04-06 ------------------- This patch relaxes constraints in our tests on the expected values returned by the standard library function :func:`~python:math.hypot` and the internal helper function ``cathetus``, to fix near-exact test failures on some 32-bit systems used by downstream packagers. .. _v3.55.0: ------------------- 3.55.0 - 2018-04-05 ------------------- This release includes several improvements to the handling of the :obj:`~hypothesis.settings.database` setting. - The ``database_file`` setting was a historical artefact, and you should just use :obj:`~hypothesis.settings.database` directly. - The ``HYPOTHESIS_DATABASE_FILE`` environment variable is deprecated, in favor of :meth:`~hypothesis.settings.load_profile` and the :obj:`~hypothesis.settings.database` setting. - If you have not configured the example database at all and the default location is not usable (due to e.g. permissions issues), Hypothesis will fall back to an in-memory database. This is not persisted between sessions, but means that the defaults work on read-only filesystems. .. _v3.54.0: ------------------- 3.54.0 - 2018-04-04 ------------------- This release improves the :func:`~hypothesis.strategies.complex_numbers` strategy, which now supports ``min_magnitude`` and ``max_magnitude`` arguments, along with ``allow_nan`` and ``allow_infinity`` like for :func:`~hypothesis.strategies.floats`. Thanks to J.J. Green for this feature. .. _v3.53.0: ------------------- 3.53.0 - 2018-04-01 ------------------- This release removes support for Django 1.8, which reached end of life on 2018-04-01. You can see Django's release and support schedule `on the Django Project website `_. .. _v3.52.3: ------------------- 3.52.3 - 2018-04-01 ------------------- This patch fixes the ``min_satisfying_examples`` settings documentation, by explaining that example shrinking is tracked at the level of the underlying bytestream rather than the output value. The output from ``find()`` in verbose mode has also been adjusted - see the example session for |Verbosity| - to avoid duplicating lines when the example repr is constant, even if the underlying representation has been shrunken. .. _v3.52.2: ------------------- 3.52.2 - 2018-03-30 ------------------- This release improves the output of failures with :ref:`rule based stateful testing ` in two ways: * The output from it is now usually valid Python code. * When the same value has two different names because it belongs to two different bundles, it will now display with the name associated with the correct bundle for a rule argument where it is used. .. _v3.52.1: ------------------- 3.52.1 - 2018-03-29 ------------------- This release improves the behaviour of :ref:`stateful testing ` in two ways: * Previously some runs would run no steps (:issue:`376`). This should no longer happen. * RuleBasedStateMachine tests which used bundles extensively would often shrink terribly. This should now be significantly improved, though there is likely a lot more room for improvement. This release also involves a low level change to how ranges of integers are handles which may result in other improvements to shrink quality in some cases. .. _v3.52.0: ------------------- 3.52.0 - 2018-03-24 ------------------- This release deprecates use of :func:`@settings(...) ` as a decorator, on functions or methods that are not also decorated with :func:`@given `. You can still apply these decorators in any order, though you should only do so once each. Applying :func:`@given ` twice was already deprecated, and applying :func:`@settings(...) ` twice is deprecated in this release and will become an error in a future version. Neither could ever be used twice to good effect. Using :func:`@settings(...) ` as the sole decorator on a test is completely pointless, so this common usage error will become an error in a future version of Hypothesis. .. _v3.51.0: ------------------- 3.51.0 - 2018-03-24 ------------------- This release deprecates the ``average_size`` argument to :func:`~hypothesis.strategies.lists` and other collection strategies. You should simply delete it wherever it was used in your tests, as it no longer has any effect. In early versions of Hypothesis, the ``average_size`` argument was treated as a hint about the distribution of examples from a strategy. Subsequent improvements to the conceptual model and the engine for generating and shrinking examples mean it is more effective to simply describe what constitutes a valid example, and let our internals handle the distribution. .. _v3.50.3: ------------------- 3.50.3 - 2018-03-24 ------------------- This patch contains some internal refactoring so that we can run with warnings as errors in CI. .. _v3.50.2: ------------------- 3.50.2 - 2018-03-20 ------------------- This has no user-visible changes except one slight formatting change to one docstring, to avoid a deprecation warning. .. _v3.50.1: ------------------- 3.50.1 - 2018-03-20 ------------------- This patch fixes an internal error introduced in :ref:`3.48.0 `, where a check for the Django test runner would expose import-time errors in Django configuration (:issue:`1167`). .. _v3.50.0: ------------------- 3.50.0 - 2018-03-19 ------------------- This release improves validation of numeric bounds for some strategies. - :func:`~hypothesis.strategies.integers` and :func:`~hypothesis.strategies.floats` now raise ``InvalidArgument`` if passed a ``min_value`` or ``max_value`` which is not an instance of :class:`~python:numbers.Real`, instead of various internal errors. - :func:`~hypothesis.strategies.floats` now converts its bounding values to the nearest float above or below the min or max bound respectively, instead of just casting to float. The old behaviour was incorrect in that you could generate ``float(min_value)``, even when this was less than ``min_value`` itself (possible with eg. fractions). - When both bounds are provided to :func:`~hypothesis.strategies.floats` but there are no floats in the interval, such as ``[(2**54)+1 .. (2**55)-1]``, InvalidArgument is raised. - :func:`~hypothesis.strategies.decimals` gives a more useful error message if passed a string that cannot be converted to :class:`~python:decimal.Decimal` in a context where this error is not trapped. Code that previously **seemed** to work may be explicitly broken if there were no floats between ``min_value`` and ``max_value`` (only possible with non-float bounds), or if a bound was not a :class:`~python:numbers.Real` number but still allowed in :obj:`python:math.isnan` (some custom classes with a ``__float__`` method). .. _v3.49.1: ------------------- 3.49.1 - 2018-03-15 ------------------- This patch fixes our tests for Numpy dtype strategies on big-endian platforms, where the strategy behaved correctly but the test assumed that the native byte order was little-endian. There is no user impact unless you are running our test suite on big-endian platforms. Thanks to Graham Inggs for reporting :issue:`1164`. .. _v3.49.0: ------------------- 3.49.0 - 2018-03-12 ------------------- This release deprecates passing ``elements=None`` to collection strategies, such as :func:`~hypothesis.strategies.lists`. Requiring ``lists(nothing())`` or ``builds(list)`` instead of ``lists()`` means slightly more typing, but also improves the consistency and discoverability of our API - as well as showing how to compose or construct strategies in ways that still work in more complex situations. Passing a nonzero max_size to a collection strategy where the elements strategy contains no values is now deprecated, and will be an error in a future version. The equivalent with ``elements=None`` is already an error. .. _v3.48.1: ------------------- 3.48.1 - 2018-03-05 ------------------- This patch will minimize examples that would come out non-minimal in previous versions. Thanks to Kyle Reeve for this patch. .. _v3.48.0: ------------------- 3.48.0 - 2018-03-05 ------------------- This release improves some "unhappy paths" when using Hypothesis with the standard library :mod:`python:unittest` module: - Applying :func:`@given ` to a non-test method which is overridden from :class:`python:unittest.TestCase`, such as ``setUp``, raises :attr:`a new health check `. (:issue:`991`) - Using :meth:`~python:unittest.TestCase.subTest` within a test decorated with :func:`@given ` would leak intermediate results when tests were run under the :mod:`python:unittest` test runner. Individual reporting of failing subtests is now disabled during a test using :func:`@given `. (:issue:`1071`) - :func:`@given ` is still not a class decorator, but the error message if you try using it on a class has been improved. As a related improvement, using :class:`django:django.test.TestCase` with :func:`@given ` instead of :class:`hypothesis.extra.django.TestCase` raises an explicit error instead of running all examples in a single database transaction. .. _v3.47.0: ------------------- 3.47.0 - 2018-03-02 ------------------- :obj:`~hypothesis.settings.register_profile` now accepts keyword arguments for specific settings, and the parent settings object is now optional. Using a ``name`` for a registered profile which is not a string was never suggested, but it is now also deprecated and will eventually be an error. .. _v3.46.2: ------------------- 3.46.2 - 2018-03-01 ------------------- This release removes an unnecessary branch from the code, and has no user-visible impact. .. _v3.46.1: ------------------- 3.46.1 - 2018-03-01 ------------------- This changes only the formatting of our docstrings and should have no user-visible effects. .. _v3.46.0: ------------------- 3.46.0 - 2018-02-26 ------------------- :func:`~hypothesis.strategies.characters` has improved docs about what arguments are valid, and additional validation logic to raise a clear error early (instead of e.g. silently ignoring a bad argument). Categories may be specified as the Unicode 'general category' (eg ``'Nd'``), or as the 'major category' (eg ``['N', 'Lu']`` is equivalent to ``['Nd', 'Nl', 'No', 'Lu']``). In previous versions, general categories were supported and all other input was silently ignored. Now, major categories are supported in addition to general categories (which may change the behaviour of some existing code), and all other input is deprecated. .. _v3.45.5: ------------------- 3.45.5 - 2018-02-26 ------------------- This patch improves strategy inference in ``hypothesis.extra.django`` to account for some validators in addition to field type - see :issue:`1116` for ongoing work in this space. Specifically, if a :class:`~django:django.db.models.CharField` or :class:`~django:django.db.models.TextField` has an attached :class:`~django:django.core.validators.RegexValidator`, we now use :func:`~hypothesis.strategies.from_regex` instead of :func:`~hypothesis.strategies.text` as the underlying strategy. This allows us to generate examples of the default :class:`~django:django.contrib.auth.models.User` model, closing :issue:`1112`. .. _v3.45.4: ------------------- 3.45.4 - 2018-02-25 ------------------- This patch improves some internal debugging information, fixes a typo in a validation error message, and expands the documentation for new contributors. .. _v3.45.3: ------------------- 3.45.3 - 2018-02-23 ------------------- This patch may improve example shrinking slightly for some strategies. .. _v3.45.2: ------------------- 3.45.2 - 2018-02-18 ------------------- This release makes our docstring style more consistent, thanks to :pypi:`flake8-docstrings`. There are no user-visible changes. .. _v3.45.1: ------------------- 3.45.1 - 2018-02-17 ------------------- This fixes an indentation issue in docstrings for :func:`~hypothesis.strategies.datetimes`, :func:`~hypothesis.strategies.dates`, :func:`~hypothesis.strategies.times`, and :func:`~hypothesis.strategies.timedeltas`. .. _v3.45.0: ------------------- 3.45.0 - 2018-02-13 ------------------- This release fixes :func:`~hypothesis.strategies.builds` so that ``target`` can be used as a keyword argument for passing values to the target. The target itself can still be specified as a keyword argument, but that behavior is now deprecated. The target should be provided as the first positional argument. .. _v3.44.26: -------------------- 3.44.26 - 2018-02-06 -------------------- This release fixes some formatting issues in the Hypothesis source code. It should have no externally visible effects. .. _v3.44.25: -------------------- 3.44.25 - 2018-02-05 -------------------- This release changes the way in which Hypothesis tries to shrink the size of examples. It probably won't have much impact, but might make shrinking faster in some cases. It is unlikely but not impossible that it will change the resulting examples. .. _v3.44.24: -------------------- 3.44.24 - 2018-01-27 -------------------- This release fixes dependency information when installing Hypothesis from a binary "wheel" distribution. - The ``install_requires`` for :pypi:`enum34` is resolved at install time, rather than at build time (with potentially different results). - Django has fixed their ``python_requires`` for versions 2.0.0 onward, simplifying Python2-compatible constraints for downstream projects. .. _v3.44.23: -------------------- 3.44.23 - 2018-01-24 -------------------- This release improves shrinking in a class of pathological examples that you are probably never hitting in practice. If you *are* hitting them in practice this should be a significant speed up in shrinking. If you are not, you are very unlikely to notice any difference. You might see a slight slow down and/or slightly better falsifying examples. .. _v3.44.22: -------------------- 3.44.22 - 2018-01-23 -------------------- This release fixes a dependency problem. It was possible to install Hypothesis with an old version of :pypi:`attrs`, which would throw a ``TypeError`` as soon as you tried to import hypothesis. Specifically, you need attrs 16.0.0 or newer. Hypothesis will now require the correct version of attrs when installing. .. _v3.44.21: -------------------- 3.44.21 - 2018-01-22 -------------------- This change adds some additional structural information that Hypothesis will use to guide its search. You mostly shouldn't see much difference from this. The two most likely effects you would notice are: 1. Hypothesis stores slightly more examples in its database for passing tests. 2. Hypothesis *may* find new bugs that it was previously missing, but it probably won't (this is a basic implementation of the feature that is intended to support future work. Although it is useful on its own, it's not *very* useful on its own). .. _v3.44.20: -------------------- 3.44.20 - 2018-01-21 -------------------- This is a small refactoring release that changes how Hypothesis tracks some information about the boundary of examples in its internal representation. You are unlikely to see much difference in behaviour, but memory usage and run time may both go down slightly during normal test execution, and when failing Hypothesis might print its failing example slightly sooner. .. _v3.44.19: -------------------- 3.44.19 - 2018-01-21 -------------------- This changes how we compute the default ``average_size`` for all collection strategies. Previously setting a ``max_size`` without setting an ``average_size`` would have the seemingly paradoxical effect of making data generation *slower*, because it would raise the average size from its default. Now setting ``max_size`` will either leave the default unchanged or lower it from its default. If you are currently experiencing this problem, this may make your tests substantially faster. If you are not, this will likely have no effect on you. .. _v3.44.18: -------------------- 3.44.18 - 2018-01-20 -------------------- This is a small refactoring release that changes how Hypothesis detects when the structure of data generation depends on earlier values generated (e.g. when using |.flatmap| or :func:`~hypothesis.strategies.composite`). It should not have any observable effect on behaviour. .. _v3.44.17: -------------------- 3.44.17 - 2018-01-15 -------------------- This release fixes a typo in internal documentation, and has no user-visible impact. .. _v3.44.16: -------------------- 3.44.16 - 2018-01-13 -------------------- This release improves test case reduction for recursive data structures. Hypothesis now guarantees that whenever a strategy calls itself recursively (usually this will happen because you are using :func:`~hypothesis.strategies.deferred`), any recursive call may replace the top level value. e.g. given a tree structure, Hypothesis will always try replacing it with a subtree. Additionally this introduces a new heuristic that may in some circumstances significantly speed up test case reduction - Hypothesis should be better at immediately replacing elements drawn inside another strategy with their minimal possible value. .. _v3.44.15: -------------------- 3.44.15 - 2018-01-13 -------------------- :func:`~hypothesis.strategies.from_type` can now resolve recursive types such as binary trees (:issue:`1004`). Detection of non-type arguments has also improved, leading to better error messages in many cases involving :pep:`forward references <484#forward-references>`. .. _v3.44.14: -------------------- 3.44.14 - 2018-01-08 -------------------- This release fixes a bug in the shrinker that prevented the optimisations in :ref:`3.44.6 ` from working in some cases. It would not have worked correctly when filtered examples were nested (e.g. with a set of integers in some range). This would not have resulted in any correctness problems, but shrinking may have been slower than it otherwise could be. .. _v3.44.13: -------------------- 3.44.13 - 2018-01-08 -------------------- This release changes the average bit length of values drawn from :func:`~hypothesis.strategies.integers` to be much smaller. Additionally it changes the shrinking order so that now size is considered before sign - e.g. -1 will be preferred to +10. The new internal format for integers required some changes to the minimizer to make work well, so you may also see some improvements to example quality in unrelated areas. .. _v3.44.12: -------------------- 3.44.12 - 2018-01-07 -------------------- This changes Hypothesis's internal implementation of weighted sampling. This will affect example distribution and quality, but you shouldn't see any other effects. .. _v3.44.11: -------------------- 3.44.11 - 2018-01-06 -------------------- This is a change to some internals around how Hypothesis handles avoiding generating duplicate examples and seeking out novel regions of the search space. You are unlikely to see much difference as a result of it, but it fixes a bug where an internal assertion could theoretically be triggered and has some minor effects on the distribution of examples so could potentially find bugs that have previously been missed. .. _v3.44.10: -------------------- 3.44.10 - 2018-01-06 -------------------- This patch avoids creating debug statements when debugging is disabled. Profiling suggests this is a 5-10% performance improvement (:issue:`1040`). .. _v3.44.9: ------------------- 3.44.9 - 2018-01-06 ------------------- This patch blacklists null characters (``'\x00'``) in automatically created strategies for Django :obj:`~django:django.db.models.CharField` and :obj:`~django:django.db.models.TextField`, due to a database issue which `was recently fixed upstream `_ (Hypothesis :issue:`1045`). .. _v3.44.8: ------------------- 3.44.8 - 2018-01-06 ------------------- This release makes the Hypothesis shrinker slightly less greedy in order to avoid local minima - when it gets stuck, it makes a small attempt to search around the final example it would previously have returned to find a new starting point to shrink from. This should improve example quality in some cases, especially ones where the test data has dependencies among parts of it that make it difficult for Hypothesis to proceed. .. _v3.44.7: ------------------- 3.44.7 - 2018-01-04 ------------------- This release adds support for `Django 2 `_ in the hypothesis-django extra. This release drops support for Django 1.10, as it is no longer supported by the Django team. .. _v3.44.6: ------------------- 3.44.6 - 2018-01-02 ------------------- This release speeds up test case reduction in many examples by being better at detecting large shrinks it can use to discard redundant parts of its input. This will be particularly noticeable in examples that make use of filtering and for some integer ranges. .. _v3.44.5: ------------------- 3.44.5 - 2018-01-02 ------------------- Happy new year! This is a no-op release that updates the year range on all of the copyright headers in our source to include 2018. .. _v3.44.4: ------------------- 3.44.4 - 2017-12-23 ------------------- This release fixes :issue:`1041`, which slowed tests by up to 6% due to broken caching. .. _v3.44.3: ------------------- 3.44.3 - 2017-12-21 ------------------- This release improves the shrinker in cases where examples drawn earlier can affect how much data is drawn later (e.g. when you draw a length parameter in a composite and then draw that many elements). Examples found in cases like this should now be much closer to minimal. .. _v3.44.2: ------------------- 3.44.2 - 2017-12-20 ------------------- This is a pure refactoring release which changes how Hypothesis manages its set of examples internally. It should have no externally visible effects. .. _v3.44.1: ------------------- 3.44.1 - 2017-12-18 ------------------- This release fixes :issue:`997`, in which under some circumstances the body of tests run under Hypothesis would not show up when run under coverage even though the tests were run and the code they called outside of the test file would show up normally. .. _v3.44.0: ------------------- 3.44.0 - 2017-12-17 ------------------- This release adds a new feature: The :func:`@reproduce_failure ` decorator, designed to make it easy to use Hypothesis's binary format for examples to reproduce a problem locally without having to share your example database between machines. This also changes when seeds are printed: * They will no longer be printed for normal falsifying examples, as there are now adequate ways of reproducing those for all cases, so it just contributes noise. * They will once again be printed when reusing examples from the database, as health check failures should now be more reliable in this scenario so it will almost always work in this case. This work was funded by `Smarkets `_. .. _v3.43.1: ------------------- 3.43.1 - 2017-12-17 ------------------- This release fixes a bug with Hypothesis's database management - examples that were found in the course of shrinking were saved in a way that indicated that they had distinct causes, and so they would all be retried on the start of the next test. The intended behaviour, which is now what is implemented, is that only a bounded subset of these examples would be retried. .. _v3.43.0: ------------------- 3.43.0 - 2017-12-17 ------------------- :exc:`~hypothesis.errors.HypothesisDeprecationWarning` now inherits from :exc:`python:FutureWarning` instead of :exc:`python:DeprecationWarning`, as recommended by :pep:`565` for user-facing warnings (:issue:`618`). If you have not changed the default warnings settings, you will now see each distinct :exc:`~hypothesis.errors.HypothesisDeprecationWarning` instead of only the first. .. _v3.42.2: ------------------- 3.42.2 - 2017-12-12 ------------------- This patch fixes :issue:`1017`, where instances of a list or tuple subtype used as an argument to a strategy would be coerced to tuple. .. _v3.42.1: ------------------- 3.42.1 - 2017-12-10 ------------------- This release has some internal cleanup, which makes reading the code more pleasant and may shrink large examples slightly faster. .. _v3.42.0: ------------------- 3.42.0 - 2017-12-09 ------------------- This release deprecates ``faker-extra``, which was designed as a transition strategy but does not support example shrinking or coverage-guided discovery. .. _v3.41.0: ------------------- 3.41.0 - 2017-12-06 ------------------- :func:`~hypothesis.strategies.sampled_from` can now sample from one-dimensional numpy ndarrays. Sampling from multi-dimensional ndarrays still results in a deprecation warning. Thanks to Charlie Tanksley for this patch. .. _v3.40.1: ------------------- 3.40.1 - 2017-12-04 ------------------- This release makes two changes: * It makes the calculation of some of the metadata that Hypothesis uses for shrinking occur lazily. This should speed up performance of test case generation a bit because it no longer calculates information it doesn't need. * It improves the shrinker for certain classes of nested examples. e.g. when shrinking lists of lists, the shrinker is now able to concatenate two adjacent lists together into a single list. As a result of this change, shrinking may get somewhat slower when the minimal example found is large. .. _v3.40.0: ------------------- 3.40.0 - 2017-12-02 ------------------- This release improves how various ways of seeding Hypothesis interact with the example database: * Using the example database with :func:`~hypothesis.seed` is now deprecated. You should set ``database=None`` if you are doing that. This will only warn if you actually load examples from the database while using ``@seed``. * The :attr:`~hypothesis.settings.derandomize` will behave the same way as ``@seed``. * Using ``--hypothesis-seed`` will disable use of the database. * If a test used examples from the database, it will not suggest using a seed to reproduce it, because that won't work. This work was funded by `Smarkets `_. .. _v3.39.0: ------------------- 3.39.0 - 2017-12-01 ------------------- This release adds a new health check that checks if the smallest "natural" possible example of your test case is very large - this will tend to cause Hypothesis to generate bad examples and be quite slow. This work was funded by `Smarkets `_. .. _v3.38.9: ------------------- 3.38.9 - 2017-11-29 ------------------- This is a documentation release to improve the documentation of shrinking behaviour for Hypothesis's strategies. .. _v3.38.8: ------------------- 3.38.8 - 2017-11-29 ------------------- This release improves the performance of :func:`~hypothesis.strategies.characters` when using ``exclude_characters`` and :func:`~hypothesis.strategies.from_regex` when using negative character classes. The problems this fixes were found in the course of work funded by `Smarkets `_. .. _v3.38.7: ------------------- 3.38.7 - 2017-11-29 ------------------- This is a patch release for :func:`~hypothesis.strategies.from_regex`, which had a bug in handling of the :obj:`python:re.VERBOSE` flag (:issue:`992`). Flags are now handled correctly when parsing regex. .. _v3.38.6: ------------------- 3.38.6 - 2017-11-28 ------------------- This patch changes a few byte-string literals from double to single quotes, thanks to an update in :pypi:`unify`. There are no user-visible changes. .. _v3.38.5: ------------------- 3.38.5 - 2017-11-23 ------------------- This fixes the repr of strategies using lambda that are defined inside decorators to include the lambda source. This would mostly have been visible when using the :ref:`statistics ` functionality - lambdas used for e.g. filtering would have shown up with a ```` as their body. This can still happen, but it should happen less often now. .. _v3.38.4: ------------------- 3.38.4 - 2017-11-22 ------------------- This release updates the reported :ref:`statistics ` so that they show approximately what fraction of your test run time is spent in data generation (as opposed to test execution). This work was funded by `Smarkets `_. .. _v3.38.3: ------------------- 3.38.3 - 2017-11-21 ------------------- This is a documentation release, which ensures code examples are up to date by running them as doctests in CI (:issue:`711`). .. _v3.38.2: ------------------- 3.38.2 - 2017-11-21 ------------------- This release changes the behaviour of the :attr:`~hypothesis.settings.deadline` setting when used with :func:`~hypothesis.strategies.data`: Time spent inside calls to ``data.draw`` will no longer be counted towards the deadline time. As a side effect of some refactoring required for this work, the way flaky tests are handled has changed slightly. You are unlikely to see much difference from this, but some error messages will have changed. This work was funded by `Smarkets `_. .. _v3.38.1: ------------------- 3.38.1 - 2017-11-21 ------------------- This patch has a variety of non-user-visible refactorings, removing various minor warts ranging from indirect imports to typos in comments. .. _v3.38.0: ------------------- 3.38.0 - 2017-11-18 ------------------- This release overhauls the |HealthCheck| system in a variety of small ways. It adds no new features, but is nevertheless a minor release because it changes which tests are likely to fail health checks. The most noticeable effect is that some tests that used to fail health checks will now pass, and some that used to pass will fail. These should all be improvements in accuracy. In particular: * New failures will usually be because they are now taking into account things like use of :func:`~hypothesis.strategies.data` and :func:`~hypothesis.assume` inside the test body. * New failures *may* also be because for some classes of example the way data generation performance was measured was artificially faster than real data generation (for most examples that are hitting performance health checks the opposite should be the case). * Tests that used to fail health checks and now pass do so because the health check system used to run in a way that was subtly different than the main Hypothesis data generation and lacked some of its support for e.g. large examples. If your data generation is especially slow, you may also see your tests get somewhat faster, as there is no longer a separate health check phase. This will be particularly noticeable when rerunning test failures. This work was funded by `Smarkets `_. .. _v3.37.0: ------------------- 3.37.0 - 2017-11-12 ------------------- This is a deprecation release for some health check related features. The following are now deprecated: * Passing ``HealthCheck.exception_in_generation`` to :attr:`~hypothesis.settings.suppress_health_check`. This no longer does anything even when passed - All errors that occur during data generation will now be immediately reraised rather than going through the health check mechanism. * Passing ``HealthCheck.random_module`` to :attr:`~hypothesis.settings.suppress_health_check`. This hasn't done anything for a long time, but was never explicitly deprecated. Hypothesis always seeds the random module when running :func:`@given ` tests, so this is no longer an error and suppressing it doesn't do anything. * Passing non-:class:`~hypothesis.HealthCheck` values in :attr:`~hypothesis.settings.suppress_health_check`. This was previously allowed but never did anything useful. In addition, passing a non-iterable value as :attr:`~hypothesis.settings.suppress_health_check` will now raise an error immediately (it would never have worked correctly, but it would previously have failed later). Some validation error messages have also been updated. This work was funded by `Smarkets `_. .. _v3.36.1: ------------------- 3.36.1 - 2017-11-10 ------------------- This is a yak shaving release, mostly concerned with our own tests. While :func:`~python:inspect.getfullargspec` was documented as deprecated in Python 3.5, it never actually emitted a warning. Our code to silence this (nonexistent) warning has therefore been removed. We now run our tests with ``DeprecationWarning`` as an error, and made some minor changes to our own tests as a result. This required similar upstream updates to :pypi:`coverage` and :pypi:`execnet` (a test-time dependency via :pypi:`pytest-xdist`). There is no user-visible change in Hypothesis itself, but we encourage you to consider enabling deprecations as errors in your own tests. .. _v3.36.0: ------------------- 3.36.0 - 2017-11-06 ------------------- This release adds a setting to the public API, and does some internal cleanup: - The :attr:`~hypothesis.settings.derandomize` setting is now documented (:issue:`890`) - Removed - and disallowed - all 'bare excepts' in Hypothesis (:issue:`953`) - Documented the ``strict`` setting as deprecated, and updated the build so our docs always match deprecations in the code. .. _v3.35.0: ------------------- 3.35.0 - 2017-11-06 ------------------- This minor release supports constraining :func:`~hypothesis.strategies.uuids` to generate a particular version of :class:`~python:uuid.UUID` (:issue:`721`). Thanks to Dion Misic for this feature. .. _v3.34.1: ------------------- 3.34.1 - 2017-11-02 ------------------- This patch updates the documentation to suggest :func:`builds(callable) ` instead of :func:`just(callable()) `. .. _v3.34.0: ------------------- 3.34.0 - 2017-11-02 ------------------- Hypothesis now emits deprecation warnings if you apply :func:`@given ` more than once to a target. Applying :func:`@given ` repeatedly wraps the target multiple times. Each wrapper will search the space of possible parameters separately. This is equivalent but will be much more inefficient than doing it with a single call to :func:`@given `. For example, instead of ``@given(booleans()) @given(integers())``, you could write ``@given(booleans(), integers())`` .. _v3.33.1: ------------------- 3.33.1 - 2017-11-02 ------------------- This is a bugfix release: - :func:`~hypothesis.strategies.builds` would try to infer a strategy for required positional arguments of the target from type hints, even if they had been given to :func:`~hypothesis.strategies.builds` as positional arguments (:issue:`946`). Now it only infers missing required arguments. - An internal introspection function wrongly reported ``self`` as a required argument for bound methods, which might also have affected :func:`~hypothesis.strategies.builds`. Now it knows better. .. _v3.33.0: ------------------- 3.33.0 - 2017-10-16 ------------------- This release supports strategy inference for more Django field types - you can now omit an argument for Date, Time, Duration, Slug, IP Address, and UUID fields. (:issue:`642`) Strategy generation for fields with grouped choices now selects choices from each group, instead of selecting from the group names. .. _v3.32.2: ------------------- 3.32.2 - 2017-10-15 ------------------- This patch removes the ``mergedb`` tool, introduced in Hypothesis 1.7.1 on an experimental basis. It has never actually worked, and the new :ref:`Hypothesis example database ` is designed to make such a tool unnecessary. .. _v3.32.1: ------------------- 3.32.1 - 2017-10-13 ------------------- This patch has two improvements for strategies based on enumerations. - :func:`~hypothesis.strategies.from_type` now handles enumerations correctly, delegating to :func:`~hypothesis.strategies.sampled_from`. Previously it noted that ``Enum.__init__`` has no required arguments and therefore delegated to :func:`~hypothesis.strategies.builds`, which would subsequently fail. - When sampling from an :class:`python:enum.Flag`, we also generate combinations of members. Eg for ``Flag('Permissions', 'READ, WRITE, EXECUTE')`` we can now generate, ``Permissions.READ``, ``Permissions.READ|WRITE``, and so on. .. _v3.32.0: ------------------- 3.32.0 - 2017-10-09 ------------------- This changes the default value of the ``use_coverage`` setting to True when running on pypy (it was already True on CPython). It was previously set to False because we expected it to be too slow, but recent benchmarking shows that actually performance of the feature on pypy is fairly acceptable - sometimes it's slower than on CPython, sometimes it's faster, but it's generally within a factor of two either way. .. _v3.31.6: ------------------- 3.31.6 - 2017-10-08 ------------------- This patch improves the quality of strategies inferred from Numpy dtypes: * Integer dtypes generated examples with the upper half of their (non-sign) bits set to zero. The inferred strategies can now produce any representable integer. * Fixed-width unicode- and byte-string dtypes now cap the internal example length, which should improve example and shrink quality. * Numpy arrays can only store fixed-size strings internally, and allow shorter strings by right-padding them with null bytes. Inferred string strategies no longer generate such values, as they can never be retrieved from an array. This improves shrinking performance by skipping useless values. This has already been useful in Hypothesis - we found an overflow bug in our Pandas support, and as a result :func:`~hypothesis.extra.pandas.indexes` and :func:`~hypothesis.extra.pandas.range_indexes` now check that ``min_size`` and ``max_size`` are at least zero. .. _v3.31.5: ------------------- 3.31.5 - 2017-10-08 ------------------- This release fixes a performance problem in tests where the ``use_coverage`` setting is True. Tests experience a slow-down proportionate to the amount of code they cover. This is still the case, but the factor is now low enough that it should be unnoticeable. Previously it was large and became much larger in :ref:`3.30.4 `. .. _v3.31.4: ------------------- 3.31.4 - 2017-10-08 ------------------- :func:`~hypothesis.strategies.from_type` failed with a very confusing error if passed a :obj:`~typing.NewType` (:issue:`901`). These pseudo-types are now unwrapped correctly, and strategy inference works as expected. .. _v3.31.3: ------------------- 3.31.3 - 2017-10-06 ------------------- This release makes some small optimisations to our use of coverage that should reduce constant per-example overhead. This is probably only noticeable on examples where the test itself is quite fast. On no-op tests that don't test anything you may see up to a fourfold speed increase (which is still significantly slower than without coverage). On more realistic tests the speed up is likely to be less than that. .. _v3.31.2: ------------------- 3.31.2 - 2017-09-30 ------------------- This release fixes some formatting and small typos/grammar issues in the documentation, specifically the page docs/settings.rst, and the inline docs for the various settings. .. _v3.31.1: ------------------- 3.31.1 - 2017-09-30 ------------------- This release improves the handling of deadlines so that they act better with the shrinking process. This fixes :issue:`892`. This involves two changes: 1. The deadline is raised during the initial generation and shrinking, and then lowered to the set value for final replay. This restricts our attention to examples which exceed the deadline by a more significant margin, which increases their reliability. 2. When despite the above a test still becomes flaky because it is significantly faster on rerun than it was on its first run, the error message is now more explicit about the nature of this problem, and includes both the initial test run time and the new test run time. In addition, this release also clarifies the documentation of the deadline setting slightly to be more explicit about where it applies. This work was funded by `Smarkets `_. .. _v3.31.0: ------------------- 3.31.0 - 2017-09-29 ------------------- This release blocks installation of Hypothesis on Python 3.3, which :PEP:`reached its end of life date on 2017-09-29 <398>`. This should not be of interest to anyone but downstream maintainers - if you are affected, migrate to a secure version of Python as soon as possible or at least seek commercial support. .. _v3.30.4: ------------------- 3.30.4 - 2017-09-27 ------------------- This release makes several changes: 1. It significantly improves Hypothesis's ability to use coverage information to find interesting examples. 2. It reduces the default :attr:`~hypothesis.settings.max_examples` setting from 200 to 100. This takes advantage of the improved algorithm meaning fewer examples are typically needed to get the same testing and is sufficiently better at covering interesting behaviour, and offsets some of the performance problems of running under coverage. 3. Hypothesis will always try to start its testing with an example that is near minimized. The new algorithm for 1 also makes some changes to Hypothesis's low level data generation which apply even with coverage turned off. They generally reduce the total amount of data generated, which should improve test performance somewhat. Between this and 3 you should see a noticeable reduction in test runtime (how much so depends on your tests and how much example size affects their performance. On our benchmarks, where data generation dominates, we saw up to a factor of two performance improvement, but it's unlikely to be that large. .. _v3.30.3: ------------------- 3.30.3 - 2017-09-25 ------------------- This release fixes some formatting and small typos/grammar issues in the documentation, specifically the page docs/details.rst, and some inline docs linked from there. .. _v3.30.2: ------------------- 3.30.2 - 2017-09-24 ------------------- This release changes Hypothesis's caching approach for functions in ``hypothesis.strategies``. Previously it would have cached extremely aggressively and cache entries would never be evicted. Now it adopts a least-frequently used, least recently used key invalidation policy, and is somewhat more conservative about which strategies it caches. Workloads which create strategies based on dynamic values, e.g. by using |.flatmap| or :func:`~hypothesis.strategies.composite`, will use significantly less memory. .. _v3.30.1: ------------------- 3.30.1 - 2017-09-22 ------------------- This release fixes a bug where when running with the ``use_coverage=True`` setting inside an existing running instance of coverage, Hypothesis would frequently put files that the coveragerc excluded in the report for the enclosing coverage. .. _v3.30.0: ------------------- 3.30.0 - 2017-09-20 ------------------- This release introduces two new features: * When a test fails, either with a health check failure or a falsifying example, Hypothesis will print out a seed that led to that failure, if the test is not already running with a fixed seed. You can then recreate that failure using either the :func:`@seed ` decorator or (if you are running pytest) with ``--hypothesis-seed``. * :pypi:`pytest` users can specify a seed to use for :func:`@given ` based tests by passing the ``--hypothesis-seed`` command line argument. This work was funded by `Smarkets `_. .. _v3.29.0: ------------------- 3.29.0 - 2017-09-19 ------------------- This release makes Hypothesis coverage aware. Hypothesis now runs all test bodies under coverage, and uses this information to guide its testing. The ``use_coverage`` setting can be used to disable this behaviour if you want to test code that is sensitive to coverage being enabled (either because of performance or interaction with the trace function). The main benefits of this feature are: * Hypothesis now observes when examples it discovers cover particular lines or branches and stores them in the database for later. * Hypothesis will make some use of this information to guide its exploration of the search space and improve the examples it finds (this is currently used only very lightly and will likely improve significantly in future releases). This also has the following side-effects: * Hypothesis now has an install time dependency on the :pypi:`coverage` package. * Tests that are already running Hypothesis under coverage will likely get faster. * Tests that are not running under coverage now run their test bodies under coverage by default. This feature is only partially supported under pypy. It is significantly slower than on CPython and is turned off by default as a result, but it should still work correctly if you want to use it. .. _v3.28.3: ------------------- 3.28.3 - 2017-09-18 ------------------- This release is an internal change that affects how Hypothesis handles calculating certain properties of strategies. The primary effect of this is that it fixes a bug where use of :func:`~hypothesis.strategies.deferred` could sometimes trigger an internal assertion error. However the fix for this bug involved some moderately deep changes to how Hypothesis handles certain constructs so you may notice some additional knock-on effects. In particular the way Hypothesis handles drawing data from strategies that cannot generate any values has changed to bail out sooner than it previously did. This may speed up certain tests, but it is unlikely to make much of a difference in practice for tests that were not already failing with Unsatisfiable. .. _v3.28.2: ------------------- 3.28.2 - 2017-09-18 ------------------- This is a patch release that fixes a bug in the :mod:`hypothesis.extra.pandas` documentation where it incorrectly referred to :func:`~hypothesis.extra.pandas.column` instead of :func:`~hypothesis.extra.pandas.columns`. .. _v3.28.1: ------------------- 3.28.1 - 2017-09-16 ------------------- This is a refactoring release. It moves a number of internal uses of :func:`~python:collections.namedtuple` over to using attrs based classes, and removes a couple of internal namedtuple classes that were no longer in use. It should have no user visible impact. .. _v3.28.0: ------------------- 3.28.0 - 2017-09-15 ------------------- This release adds support for testing :pypi:`pandas` via the :ref:`hypothesis.extra.pandas ` module. It also adds a dependency on :pypi:`attrs`. This work was funded by `Stripe `_. .. _v3.27.1: ------------------- 3.27.1 - 2017-09-14 ------------------- This release fixes some formatting and broken cross-references in the documentation, which includes editing docstrings - and thus a patch release. .. _v3.27.0: ------------------- 3.27.0 - 2017-09-13 ------------------- This release introduces a :attr:`~hypothesis.settings.deadline` setting to Hypothesis. When set this turns slow tests into errors. By default it is unset but will warn if you exceed 200ms, which will become the default value in a future release. This work was funded by `Smarkets `_. .. _v3.26.0: ------------------- 3.26.0 - 2017-09-12 ------------------- Hypothesis now emits deprecation warnings if you are using the legacy SQLite example database format, or the tool for merging them. These were already documented as deprecated, so this doesn't change their deprecation status, only that we warn about it. .. _v3.25.1: ------------------- 3.25.1 - 2017-09-12 ------------------- This release fixes a bug with generating :doc:`numpy datetime and timedelta types `: When inferring the strategy from the dtype, datetime and timedelta dtypes with sub-second precision would always produce examples with one second resolution. Inferring a strategy from a time dtype will now always produce example with the same precision. .. _v3.25.0: ------------------- 3.25.0 - 2017-09-12 ------------------- This release changes how Hypothesis shrinks and replays examples to take into account that it can encounter new bugs while shrinking the bug it originally found. Previously it would end up replacing the originally found bug with the new bug and show you only that one. Now it is (often) able to recognise when two bugs are distinct and when it finds more than one will show both. .. _v3.24.2: ------------------- 3.24.2 - 2017-09-11 ------------------- This release removes the (purely internal and no longer useful) ``strategy_test_suite`` function and the corresponding strategytests module. .. _v3.24.1: ------------------- 3.24.1 - 2017-09-06 ------------------- This release improves the reduction of examples involving floating point numbers to produce more human readable examples. It also has some general purpose changes to the way the minimizer works internally, which may see some improvement in quality and slow down of test case reduction in cases that have nothing to do with floating point numbers. .. _v3.24.0: ------------------- 3.24.0 - 2017-09-05 ------------------- Hypothesis now emits deprecation warnings if you use ``some_strategy.example()`` inside a test function or strategy definition (this was never intended to be supported, but is sufficiently widespread that it warrants a deprecation path). .. _v3.23.3: ------------------- 3.23.3 - 2017-09-05 ------------------- This is a bugfix release for :func:`~hypothesis.strategies.decimals` with the ``places`` argument. - No longer fails health checks (:issue:`725`, due to internal filtering) - Specifying a ``min_value`` and ``max_value`` without any decimals with ``places`` places between them gives a more useful error message. - Works for any valid arguments, regardless of the decimal precision context. .. _v3.23.2: ------------------- 3.23.2 - 2017-09-01 ------------------- This is a small refactoring release that removes a now-unused parameter to an internal API. It shouldn't have any user visible effect. .. _v3.23.1: ------------------- 3.23.1 - 2017-09-01 ------------------- Hypothesis no longer propagates the dynamic scope of settings into strategy definitions. This release is a small change to something that was never part of the public API and you will almost certainly not notice any effect unless you're doing something surprising, but for example the following code will now give a different answer in some circumstances: .. code-block:: python import hypothesis.strategies as st from hypothesis import settings CURRENT_SETTINGS = st.builds(lambda: settings.default) (We don't actually encourage you writing code like this) Previously this would have generated the settings that were in effect at the point of definition of ``CURRENT_SETTINGS``. Now it will generate the settings that are used for the current test. It is very unlikely to be significant enough to be visible, but you may also notice a small performance improvement. .. _v3.23.0: ------------------- 3.23.0 - 2017-08-31 ------------------- This release adds a ``unique`` argument to :func:`~hypothesis.extra.numpy.arrays` which behaves the same ways as the corresponding one for :func:`~hypothesis.strategies.lists`, requiring all of the elements in the generated array to be distinct. .. _v3.22.2: ------------------- 3.22.2 - 2017-08-29 ------------------- This release fixes an issue where Hypothesis would raise a ``TypeError`` when using the datetime-related strategies if running with ``PYTHONOPTIMIZE=2``. This bug was introduced in :ref:`3.20.0 `. (See :issue:`822`) .. _v3.22.1: ------------------- 3.22.1 - 2017-08-28 ------------------- Hypothesis now transparently handles problems with an internal unicode cache, including file truncation or read-only filesystems (:issue:`767`). Thanks to Sam Hames for the patch. .. _v3.22.0: ------------------- 3.22.0 - 2017-08-26 ------------------- This release provides what should be a substantial performance improvement to numpy arrays generated using :ref:`provided numpy support `, and adds a new ``fill_value`` argument to :func:`~hypothesis.extra.numpy.arrays` to control this behaviour. This work was funded by `Stripe `_. .. _v3.21.3: ------------------- 3.21.3 - 2017-08-26 ------------------- This release fixes some extremely specific circumstances that probably have never occurred in the wild where users of :func:`~hypothesis.strategies.deferred` might have seen a :class:`python:RuntimeError` from too much recursion, usually in cases where no valid example could have been generated anyway. .. _v3.21.2: ------------------- 3.21.2 - 2017-08-25 ------------------- This release fixes some minor bugs in argument validation: * :ref:`hypothesis.extra.numpy ` dtype strategies would raise an internal error instead of an InvalidArgument exception when passed an invalid endianness specification. * :func:`~hypothesis.strategies.fractions` would raise an internal error instead of an InvalidArgument if passed ``float("nan")`` as one of its bounds. * The error message for passing ``float("nan")`` as a bound to various strategies has been improved. * Various bound arguments will now raise InvalidArgument in cases where they would previously have raised an internal TypeError or ValueError from the relevant conversion function. * ``streaming()`` would not have emitted a deprecation warning when called with an invalid argument. .. _v3.21.1: ------------------- 3.21.1 - 2017-08-24 ------------------- This release fixes a bug where test failures that were the result of an :obj:`@example ` would print an extra stack trace before re-raising the exception. .. _v3.21.0: ------------------- 3.21.0 - 2017-08-23 ------------------- This release deprecates Hypothesis's strict mode, which turned Hypothesis's deprecation warnings into errors. Similar functionality can be achieved by using :func:`simplefilter('error', HypothesisDeprecationWarning) `. .. _v3.20.0: ------------------- 3.20.0 - 2017-08-22 ------------------- This release renames the relevant arguments on the :func:`~hypothesis.strategies.datetimes`, :func:`~hypothesis.strategies.dates`, :func:`~hypothesis.strategies.times`, and :func:`~hypothesis.strategies.timedeltas` strategies to ``min_value`` and ``max_value``, to make them consistent with the other strategies in the module. The old argument names are still supported but will emit a deprecation warning when used explicitly as keyword arguments. Arguments passed positionally will go to the new argument names and are not deprecated. .. _v3.19.3: ------------------- 3.19.3 - 2017-08-22 ------------------- This release provides a major overhaul to the internals of how Hypothesis handles shrinking. This should mostly be visible in terms of getting better examples for tests which make heavy use of :func:`~hypothesis.strategies.composite`, :func:`~hypothesis.strategies.data` or |.flatmap| where the data drawn depends a lot on previous choices, especially where size parameters are affected. Previously Hypothesis would have struggled to reliably produce good examples here. Now it should do much better. Performance should also be better for examples with a non-zero ``min_size``. You may see slight changes to example generation (e.g. improved example diversity) as a result of related changes to internals, but they are unlikely to be significant enough to notice. .. _v3.19.2: ------------------- 3.19.2 - 2017-08-21 ------------------- This release fixes two bugs in :mod:`hypothesis.extra.numpy`: * :func:`~hypothesis.extra.numpy.unicode_string_dtypes` didn't work at all due to an incorrect dtype specifier. Now it does. * Various impossible conditions would have been accepted but would error when they fail to produced any example. Now they raise an explicit InvalidArgument error. .. _v3.19.1: ------------------- 3.19.1 - 2017-08-21 ------------------- This is a bugfix release for :issue:`739`, where bounds for :func:`~hypothesis.strategies.fractions` or floating-point :func:`~hypothesis.strategies.decimals` were not properly converted to integers before passing them to the integers strategy. This excluded some values that should have been possible, and could trigger internal errors if the bounds lay between adjacent integers. You can now bound :func:`~hypothesis.strategies.fractions` with two arbitrarily close fractions. It is now an explicit error to supply a min_value, max_value, and max_denominator to :func:`~hypothesis.strategies.fractions` where the value bounds do not include a fraction with denominator at most max_denominator. .. _v3.19.0: ------------------- 3.19.0 - 2017-08-20 ------------------- This release adds the :func:`~hypothesis.strategies.from_regex` strategy, which generates strings that contain a match of a regular expression. Thanks to Maxim Kulkin for creating the `hypothesis-regex `_ package and then helping to upstream it! (:issue:`662`) .. _v3.18.5: ------------------- 3.18.5 - 2017-08-18 ------------------- This is a bugfix release for :func:`~hypothesis.strategies.integers`. Previously the strategy would hit an internal assertion if passed non-integer bounds for ``min_value`` and ``max_value`` that had no integers between them. The strategy now raises InvalidArgument instead. .. _v3.18.4: ------------------- 3.18.4 - 2017-08-18 ------------------- Release to fix a bug where mocks can be used as test runners under certain conditions. Specifically, if a mock is injected into a test via pytest fixtures or patch decorators, and that mock is the first argument in the list, hypothesis will think it represents self and turns the mock into a test runner. If this happens, the affected test always passes because the mock is executed instead of the test body. Sometimes, it will also fail health checks. Fixes :issue:`491` and a section of :issue:`198`. Thanks to Ben Peterson for this bug fix. .. _v3.18.3: ------------------- 3.18.3 - 2017-08-17 ------------------- This release should improve the performance of some tests which experienced a slow down as a result of the :ref:`3.13.0 ` release. Tests most likely to benefit from this are ones that make extensive use of ``min_size`` parameters, but others may see some improvement as well. .. _v3.18.2: ------------------- 3.18.2 - 2017-08-16 ------------------- This release fixes a bug introduced in :ref:`3.18.0 `. If the arguments ``include_characters`` and ``exclude_characters`` to :func:`~hypothesis.strategies.characters` contained overlapping elements, then an ``InvalidArgument`` exception would be raised. Thanks to Zac Hatfield-Dodds for reporting and fixing this. .. _v3.18.1: ------------------- 3.18.1 - 2017-08-14 ------------------- This is a bug fix release to fix :issue:`780`, where :func:`~hypothesis.strategies.sets` and similar would trigger health check errors if their element strategy could only produce one element (e.g. if it was :func:`~hypothesis.strategies.just`). .. _v3.18.0: ------------------- 3.18.0 - 2017-08-13 ------------------- This is a feature release: * :func:`~hypothesis.strategies.characters` now accepts ``include_characters``, particular characters which will be added to those it produces. (:issue:`668`) * A bug fix for the internal function ``_union_interval_lists()``, and a rename to ``_union_intervals()``. It now correctly handles all cases where intervals overlap, and it always returns the result as a tuple for tuples. Thanks to Alex Willmer for these. .. _v3.17.0: ------------------- 3.17.0 - 2017-08-07 ------------------- This release documents the previously undocumented phases feature in |Phase|, making it part of the public API. It also updates how the example database is used. Principally: * The :obj:`~hypothesis.Phase.reuse` phase will now correctly control whether examples from the database are run (it previously did exactly the wrong thing and controlled whether examples would be *saved*). * Hypothesis will no longer try to rerun *all* previously failing examples. Instead it will replay the smallest previously failing example and a selection of other examples that are likely to trigger any other bugs that will found. This prevents a previous failure from dominating your tests unnecessarily. * As a result of the previous change, Hypothesis will be slower about clearing out old examples from the database that are no longer failing (because it can only clear out ones that it actually runs). .. _v3.16.1: ------------------- 3.16.1 - 2017-08-07 ------------------- This release makes an implementation change to how Hypothesis handles certain internal constructs. The main effect you should see is improvement to the behaviour and performance of collection types, especially ones with a ``min_size`` parameter. Many cases that would previously fail due to being unable to generate enough valid examples will now succeed, and other cases should run slightly faster. .. _v3.16.0: ------------------- 3.16.0 - 2017-08-04 ------------------- This release introduces a deprecation of the timeout feature. This results in the following changes: * Creating a settings object with an explicit timeout will emit a deprecation warning. * If your test stops because it hits the timeout (and has not found a bug) then it will emit a deprecation warning. * There is a new value ``unlimited`` which you can import from hypothesis. ``settings(timeout=unlimited)`` will *not* cause a deprecation warning. * There is a new health check, ``hung_test``, which will trigger after a test has been running for five minutes if it is not suppressed. .. _v3.15.0: ------------------- 3.15.0 - 2017-08-04 ------------------- This release deprecates two strategies, ``choices()`` and ``streaming()``. Both of these are somewhat confusing to use and are entirely redundant since the introduction of the :func:`~hypothesis.strategies.data` strategy for interactive drawing in tests, and their use should be replaced with direct use of :func:`~hypothesis.strategies.data` instead. .. _v3.14.2: ------------------- 3.14.2 - 2017-08-03 ------------------- This fixes a bug where Hypothesis would not work correctly on Python 2.7 if you had the :mod:`python:typing` module :pypi:`backport ` installed. .. _v3.14.1: ------------------- 3.14.1 - 2017-08-02 ------------------- This raises the maximum depth at which Hypothesis starts cutting off data generation to a more reasonable value which it is harder to hit by accident. This resolves (:issue:`751`), in which some examples which previously worked would start timing out, but it will also likely improve the data generation quality for complex data types. .. _v3.14.0: ------------------- 3.14.0 - 2017-07-23 ------------------- Hypothesis now understands inline type annotations (:issue:`293`): - If the target of :func:`~hypothesis.strategies.builds` has type annotations, a default strategy for missing required arguments is selected based on the type. Type-based strategy selection will only override a default if you pass :const:`hypothesis.infer` as a keyword argument. - If :func:`@given ` wraps a function with type annotations, you can pass :const:`~hypothesis.infer` as a keyword argument and the appropriate strategy will be substituted. - You can check what strategy will be inferred for a type with the new :func:`~hypothesis.strategies.from_type` function. - :func:`~hypothesis.strategies.register_type_strategy` teaches Hypothesis which strategy to infer for custom or unknown types. You can provide a strategy, or for more complex cases a function which takes the type and returns a strategy. .. _v3.13.1: ------------------- 3.13.1 - 2017-07-20 ------------------- This is a bug fix release for :issue:`514` - Hypothesis would continue running examples after a :class:`~python:unittest.SkipTest` exception was raised, including printing a falsifying example. Skip exceptions from the standard :mod:`python:unittest` module, and ``pytest``, ``nose``, or ``unittest2`` modules now abort the test immediately without printing output. .. _v3.13.0: ------------------- 3.13.0 - 2017-07-16 ------------------- This release has two major aspects to it: The first is the introduction of :func:`~hypothesis.strategies.deferred`, which allows more natural definition of recursive (including mutually recursive) strategies. The second is a number of engine changes designed to support this sort of strategy better. These should have a knock-on effect of also improving the performance of any existing strategies that currently generate a lot of data or involve heavy nesting by reducing their typical example size. .. _v3.12.0: ------------------- 3.12.0 - 2017-07-07 ------------------- This release makes some major internal changes to how Hypothesis represents data internally, as a prelude to some major engine changes that should improve data quality. There are no API changes, but it's a significant enough internal change that a minor version bump seemed warranted. User facing impact should be fairly mild, but includes: * All existing examples in the database will probably be invalidated. Hypothesis handles this automatically, so you don't need to do anything, but if you see all your examples disappear that's why. * Almost all data distributions have changed significantly. Possibly for the better, possibly for the worse. This may result in new bugs being found, but it may also result in Hypothesis being unable to find bugs it previously did. * Data generation may be somewhat faster if your existing bottleneck was in draw_bytes (which is often the case for large examples). * Shrinking will probably be slower, possibly significantly. If you notice any effects you consider to be a significant regression, please open an issue about them. .. _v3.11.6: ------------------- 3.11.6 - 2017-06-19 ------------------- This release involves no functionality changes, but is the first to ship wheels as well as an sdist. .. _v3.11.5: ------------------- 3.11.5 - 2017-06-18 ------------------- This release provides a performance improvement to shrinking. For cases where there is some non-trivial "boundary" value (e.g. the bug happens for all values greater than some other value), shrinking should now be substantially faster. Other types of bug will likely see improvements too. This may also result in some changes to the quality of the final examples - it may sometimes be better, but is more likely to get slightly worse in some edge cases. If you see any examples where this happens in practice, please report them. .. _v3.11.4: ------------------- 3.11.4 - 2017-06-17 ------------------- This is a bugfix release: Hypothesis now prints explicit examples when running in verbose mode. (:issue:`313`) .. _v3.11.3: ------------------- 3.11.3 - 2017-06-11 ------------------- This is a bugfix release: Hypothesis no longer emits a warning if you try to use :func:`~hypothesis.strategies.sampled_from` with :class:`python:collections.OrderedDict`. (:issue:`688`) .. _v3.11.2: ------------------- 3.11.2 - 2017-06-10 ------------------- This is a documentation release. Several outdated snippets have been updated or removed, and many cross-references are now hyperlinks. .. _v3.11.1: ------------------- 3.11.1 - 2017-05-28 ------------------- This is a minor ergonomics release. Tracebacks shown by pytest no longer include Hypothesis internals for test functions decorated with :func:`@given `. .. _v3.11.0: ------------------- 3.11.0 - 2017-05-23 ------------------- This is a feature release, adding datetime-related strategies to the core strategies. :func:`~hypothesis.extra.pytz.timezones` allows you to sample pytz timezones from the Olsen database. Use directly in a recipe for tz-aware datetimes, or compose with :func:`~hypothesis.strategies.none` to allow a mix of aware and naive output. The new :func:`~hypothesis.strategies.dates`, :func:`~hypothesis.strategies.times`, :func:`~hypothesis.strategies.datetimes`, and :func:`~hypothesis.strategies.timedeltas` strategies are all constrained by objects of their type. This means that you can generate dates bounded by a single day (i.e. a single date), or datetimes constrained to the microsecond. :func:`~hypothesis.strategies.times` and :func:`~hypothesis.strategies.datetimes` take an optional ``timezones=`` argument, which defaults to :func:`~hypothesis.strategies.none` for naive times. You can use our extra strategy based on pytz, or roll your own timezones strategy with dateutil or even the standard library. The old ``dates``, ``times``, and ``datetimes`` strategies in ``hypothesis.extra.datetimes`` are deprecated in favor of the new core strategies, which are more flexible and have no dependencies. .. _v3.10.0: ------------------- 3.10.0 - 2017-05-22 ------------------- Hypothesis now uses :func:`python:inspect.getfullargspec` internally. On Python 2, there are no visible changes. On Python 3 :func:`@given ` and :func:`@composite ` now preserve :pep:`3107` annotations on the decorated function. Keyword-only arguments are now either handled correctly (e.g. :func:`@composite `), or caught in validation instead of silently discarded or raising an unrelated error later (e.g. :func:`@given `). .. _v3.9.1: ------------------ 3.9.1 - 2017-05-22 ------------------ This is a bugfix release: the default field mapping for a DateTimeField in the Django extra now respects the ``USE_TZ`` setting when choosing a strategy. .. _v3.9.0: ------------------ 3.9.0 - 2017-05-19 ------------------ This is feature release, expanding the capabilities of the :func:`~hypothesis.strategies.decimals` strategy. * The new (optional) ``places`` argument allows you to generate decimals with a certain number of places (e.g. cents, thousandths, satoshis). * If allow_infinity is None, setting min_bound no longer excludes positive infinity and setting max_value no longer excludes negative infinity. * All of ``NaN``, ``-Nan``, ``sNaN``, and ``-sNaN`` may now be drawn if allow_nan is True, or if allow_nan is None and min_value or max_value is None. * min_value and max_value may be given as decimal strings, e.g. ``"1.234"``. .. _v3.8.5: ------------------ 3.8.5 - 2017-05-16 ------------------ Hypothesis now imports :mod:`python:sqlite3` when a SQLite database is used, rather than at module load, improving compatibility with Python implementations compiled without SQLite support (such as BSD or Jython). .. _v3.8.4: ------------------ 3.8.4 - 2017-05-16 ------------------ This is a compatibility bugfix release. :func:`~hypothesis.strategies.sampled_from` no longer raises a deprecation warning when sampling from an :class:`python:enum.Enum`, as all enums have a reliable iteration order. .. _v3.8.3: ------------------ 3.8.3 - 2017-05-09 ------------------ This release removes a version check for older versions of :pypi:`pytest` when using the Hypothesis pytest plugin. The pytest plugin will now run unconditionally on all versions of pytest. This breaks compatibility with any version of pytest prior to 2.7.0 (which is more than two years old). The primary reason for this change is that the version check was a frequent source of breakage when pytest change their versioning scheme. If you are not working on pytest itself and are not running a very old version of it, this release probably doesn't affect you. .. _v3.8.2: ------------------ 3.8.2 - 2017-04-26 ------------------ This is a code reorganisation release that moves some internal test helpers out of the main source tree so as to not have changes to them trigger releases in future. .. _v3.8.1: ------------------ 3.8.1 - 2017-04-26 ------------------ This is a documentation release. Almost all code examples are now doctests checked in CI, eliminating stale examples. .. _v3.8.0: ------------------ 3.8.0 - 2017-04-23 ------------------ This is a feature release, adding the :func:`~hypothesis.strategies.iterables` strategy, equivalent to ``lists(...).map(iter)`` but with a much more useful repr. You can use this strategy to check that code doesn't accidentally depend on sequence properties such as indexing support or repeated iteration. .. _v3.7.4: ------------------ 3.7.4 - 2017-04-22 ------------------ This patch fixes a bug in :ref:`3.7.3 `, where using :obj:`@example ` and a pytest fixture in the same test could cause the test to fail to fill the arguments, and throw a TypeError. .. _v3.7.3: ------------------ 3.7.3 - 2017-04-21 ------------------ This release should include no user visible changes and is purely a refactoring release. This modularises the behaviour of the core :func:`~hypothesis.given` function, breaking it up into smaller and more accessible parts, but its actual behaviour should remain unchanged. .. _v3.7.2: ------------------ 3.7.2 - 2017-04-21 ------------------ This reverts an undocumented change in :ref:`3.7.1 ` which broke installation on debian stable: The specifier for the hypothesis[django] extra\_requires had introduced a wild card, which was not supported on the default version of pip. .. _v3.7.1: ------------------ 3.7.1 - 2017-04-21 ------------------ This is a bug fix and internal improvements release. * In particular Hypothesis now tracks a tree of where it has already explored. This allows it to avoid some classes of duplicate examples, and significantly improves the performance of shrinking failing examples by allowing it to skip some shrinks that it can determine can't possibly work. * Hypothesis will no longer seed the global random arbitrarily unless you have asked it to using :py:meth:`~hypothesis.strategies.random_module` * Shrinking would previously have not worked correctly in some special cases on Python 2, and would have resulted in suboptimal examples. .. _v3.7.0: ------------------ 3.7.0 - 2017-03-20 ------------------ This is a feature release. New features: * Rule based stateful testing now has an :func:`@invariant ` decorator that specifies methods that are run after init and after every step, allowing you to encode properties that should be true at all times. Thanks to Tom Prince for this feature. * The :func:`~hypothesis.strategies.decimals` strategy now supports ``allow_nan`` and ``allow_infinity`` flags. * There are :ref:`significantly more strategies available for numpy `, including for generating arbitrary data types. Thanks to Zac Hatfield Dodds for this feature. * When using the :func:`~hypothesis.strategies.data` strategy you can now add a label as an argument to ``draw()``, which will be printed along with the value when an example fails. Thanks to Peter Inglesby for this feature. Bug fixes: * Bug fix: :func:`~hypothesis.strategies.composite` now preserves functions' docstrings. * The build is now reproducible and doesn't depend on the path you build it from. Thanks to Chris Lamb for this feature. * numpy strategies for the void data type did not work correctly. Thanks to Zac Hatfield Dodds for this fix. There have also been a number of performance optimizations: * The :func:`~hypothesis.strategies.permutations` strategy is now significantly faster to use for large lists (the underlying algorithm has gone from O(n^2) to O(n)). * Shrinking of failing test cases should have got significantly faster in some circumstances where it was previously struggling for a long time. * Example generation now involves less indirection, which results in a small speedup in some cases (small enough that you won't really notice it except in pathological cases). .. _v3.6.1: ------------------ 3.6.1 - 2016-12-20 ------------------ This release fixes a dependency problem and makes some small behind the scenes improvements. * The fake-factory dependency was renamed to faker. If you were depending on it through hypothesis[django] or hypothesis[fake-factory] without pinning it yourself then it would have failed to install properly. This release changes it so that hypothesis[fakefactory] (which can now also be installed as hypothesis[faker]) will install the renamed faker package instead. * This release also removed the dependency of hypothesis[django] on hypothesis[fakefactory] - it was only being used for emails. These now use a custom strategy that isn't from fakefactory. As a result you should also see performance improvements of tests which generated User objects or other things with email fields, as well as better shrinking of email addresses. * The distribution of code using nested calls to :func:`~hypothesis.strategies.one_of` or the ``|`` operator for combining strategies has been improved, as branches are now flattened to give a more uniform distribution. * Examples using :func:`~hypothesis.strategies.composite` or |.flatmap| should now shrink better. In particular this will affect things which work by first generating a length and then generating that many items, which have historically not shrunk very well. .. _v3.6.0: ------------------ 3.6.0 - 2016-10-31 ------------------ This release reverts Hypothesis to its old pretty printing of lambda functions based on attempting to extract the source code rather than decompile the bytecode. This is unfortunately slightly inferior in some cases and may result in you occasionally seeing things like ``lambda x: `` in statistics reports and strategy reprs. This removes the dependencies on uncompyle6, xdis and spark-parser. The reason for this is that the new functionality was based on uncompyle6, which turns out to introduce a hidden GPLed dependency - it in turn depended on xdis, and although the library was licensed under the MIT license, it contained some GPL licensed source code and thus should have been released under the GPL. My interpretation is that Hypothesis itself was never in violation of the GPL (because the license it is under, the Mozilla Public License v2, is fully compatible with being included in a GPL licensed work), but I have not consulted a lawyer on the subject. Regardless of the answer to this question, adding a GPLed dependency will likely cause a lot of users of Hypothesis to inadvertently be in violation of the GPL. As a result, if you are running Hypothesis 3.5.x you really should upgrade to this release immediately. .. _v3.5.3: ------------------ 3.5.3 - 2016-10-05 ------------------ This is a bug fix release. Bugs fixed: * If the same test was running concurrently in two processes and there were examples already in the test database which no longer failed, Hypothesis would sometimes fail with a FileNotFoundError (IOError on Python 2) because an example it was trying to read was deleted before it was read. (:issue:`372`). * Drawing from an :func:`~hypothesis.strategies.integers` strategy with both a min_value and a max_value would reject too many examples needlessly. Now it repeatedly redraws until satisfied. (:pull:`366`. Thanks to Calen Pennington for the contribution). .. _v3.5.2: ------------------ 3.5.2 - 2016-09-24 ------------------ This is a bug fix release. * The Hypothesis pytest plugin broke pytest support for doctests. Now it doesn't. .. _v3.5.1: ------------------ 3.5.1 - 2016-09-23 ------------------ This is a bug fix release. * Hypothesis now runs cleanly in -B and -BB modes, avoiding mixing bytes and unicode. * :class:`python:unittest.TestCase` tests would not have shown up in the new statistics mode. Now they do. * Similarly, stateful tests would not have shown up in statistics and now they do. * Statistics now print with pytest node IDs (the names you'd get in pytest verbose mode). .. _v3.5.0: ------------------ 3.5.0 - 2016-09-22 ------------------ This is a feature release. * :func:`~hypothesis.strategies.fractions` and :func:`~hypothesis.strategies.decimals` strategies now support min_value and max_value parameters. Thanks go to Anne Mulhern for the development of this feature. * The Hypothesis pytest plugin now supports a ``--hypothesis-show-statistics`` parameter that gives detailed statistics about the tests that were run. Huge thanks to Jean-Louis Fuchs and Adfinis-SyGroup for funding the development of this feature. * There is a new :func:`~hypothesis.event` function that can be used to add custom statistics. Additionally there have been some minor bug fixes: * In some cases Hypothesis should produce fewer duplicate examples (this will mostly only affect cases with a single parameter). * :pypi:`pytest` command line parameters are now under an option group for Hypothesis (thanks to David Keijser for fixing this) * Hypothesis would previously error if you used :pep:`3107` function annotations on your tests under Python 3.4. * The repr of many strategies using lambdas has been improved to include the lambda body (this was previously supported in many but not all cases). .. _v3.4.2: ------------------ 3.4.2 - 2016-07-13 ------------------ This is a bug fix release, fixing a number of problems with the settings system: * Test functions defined using :func:`@given ` can now be called from other threads (:issue:`337`) * Attempting to delete a settings property would previously have silently done the wrong thing. Now it raises an AttributeError. * Creating a settings object with a custom database_file parameter was silently getting ignored and the default was being used instead. Now it's not. .. _v3.4.1: ------------------ 3.4.1 - 2016-07-07 ------------------ This is a bug fix release for a single bug: * On Windows when running two Hypothesis processes in parallel (e.g. using :pypi:`pytest-xdist`) they could race with each other and one would raise an exception due to the non-atomic nature of file renaming on Windows and the fact that you can't rename over an existing file. This is now fixed. .. _v3.4.0: ------------------ 3.4.0 - 2016-05-27 ------------------ This release is entirely provided by `Lucas Wiman `_: Strategies constructed by the Django extra will now respect much more of Django's validations out of the box. Wherever possible, :meth:`~django:django.db.models.Model.full_clean` should succeed. In particular: * The max_length, blank and choices kwargs are now respected. * Add support for DecimalField. * If a field includes validators, the list of validators are used to filter the field strategy. .. _v3.3.0: ------------------ 3.3.0 - 2016-05-27 ------------------ This release went wrong and is functionally equivalent to :ref:`3.2.0 `. Ignore it. .. _v3.2.0: ------------------ 3.2.0 - 2016-05-19 ------------------ This is a small single-feature release: * All tests using :func:`@given ` now fix the global random seed. This removes the health check for that. If a non-zero seed is required for the final falsifying example, it will be reported. Otherwise Hypothesis will assume randomization was not a significant factor for the test and be silent on the subject. If you use :func:`~hypothesis.strategies.random_module` this will continue to work and will always display the seed. .. _v3.1.3: ------------------ 3.1.3 - 2016-05-01 ------------------ Single bug fix release * Another charmap problem. In :ref:`3.1.2 ` :func:`~hypothesis.strategies.text` and :func:`~hypothesis.strategies.characters` would break on systems which had ``/tmp`` mounted on a different partition than the Hypothesis storage directory (usually in home). This fixes that. .. _v3.1.2: ------------------ 3.1.2 - 2016-04-30 ------------------ Single bug fix release: * Anything which used a :func:`~hypothesis.strategies.text` or :func:`~hypothesis.strategies.characters` strategy was broken on Windows and I hadn't updated appveyor to use the new repository location so I didn't notice. This is now fixed and windows support should work correctly. .. _v3.1.1: ------------------ 3.1.1 - 2016-04-29 ------------------ Minor bug fix release. * Fix concurrency issue when running tests that use :func:`~hypothesis.strategies.text` from multiple processes at once (:issue:`302`, thanks to Alex Chan). * Improve performance of code using :func:`~hypothesis.strategies.lists` with max_size (thanks to Cristi Cobzarenco). * Fix install on Python 2 with ancient versions of pip so that it installs the :pypi:`enum34` backport (thanks to Donald Stufft for telling me how to do this). * Remove duplicated __all__ exports from hypothesis.strategies (thanks to Piët Delport). * Update headers to point to new repository location. * Allow use of strategies that can't be used in ``find()`` (e.g. ``choices()``) in stateful testing. .. _v3.1.0: ------------------ 3.1.0 - 2016-03-06 ------------------ * Add a :func:`~hypothesis.strategies.nothing` strategy that never successfully generates values. * :func:`~hypothesis.strategies.sampled_from` and :func:`~hypothesis.strategies.one_of` can both now be called with an empty argument list, in which case they also never generate any values. * :func:`~hypothesis.strategies.one_of` may now be called with a single argument that is a collection of strategies as well as varargs. * Add a :func:`~hypothesis.strategies.runner` strategy which returns the instance of the current test object if there is one. * 'Bundle' for RuleBasedStateMachine is now a normal(ish) strategy and can be used as such. * Tests using RuleBasedStateMachine should now shrink significantly better. * Hypothesis now uses a pretty-printing library internally, compatible with IPython's pretty printing protocol (actually using the same code). This may improve the quality of output in some cases. * Add a 'phases' setting that allows more fine grained control over which parts of the process Hypothesis runs * Add a suppress_health_check setting which allows you to turn off specific health checks in a fine grained manner. * Fix a bug where lists of non fixed size would always draw one more element than they included. This mostly didn't matter, but if would cause problems with empty strategies or ones with side effects. * Add a mechanism to the Django model generator to allow you to explicitly request the default value (thanks to Jeremy Thurgood for this one). .. _v3.0.5: ------------------ 3.0.5 - 2016-02-25 ------------------ * Fix a bug where Hypothesis would now error on :pypi:`pytest` development versions. .. _v3.0.4: ------------------ 3.0.4 - 2016-02-24 ------------------ * Fix a bug where Hypothesis would error when running on Python 2.7.3 or earlier because it was trying to pass a :class:`python:bytearray` object to :func:`python:struct.unpack` (which is only supported since 2.7.4). .. _v3.0.3: ------------------ 3.0.3 - 2016-02-23 ------------------ * Fix version parsing of pytest to work with pytest release candidates * More general handling of the health check problem where things could fail because of a cache miss - now one "free" example is generated before the start of the health check run. .. _v3.0.2: ------------------ 3.0.2 - 2016-02-18 ------------------ * Under certain circumstances, strategies involving :func:`~hypothesis.strategies.text` buried inside some other strategy (e.g. ``text().filter(...)`` or ``recursive(text(), ...))`` would cause a test to fail its health checks the first time it ran. This was caused by having to compute some related data and cache it to disk. On travis or anywhere else where the ``.hypothesis`` directory was recreated this would have caused the tests to fail their health check on every run. This is now fixed for all the known cases, although there could be others lurking. .. _v3.0.1: ------------------ 3.0.1 - 2016-02-18 ------------------ * Fix a case where it was possible to trigger an "Unreachable" assertion when running certain flaky stateful tests. * Improve shrinking of large stateful tests by eliminating a case where it was hard to delete early steps. * Improve efficiency of drawing :func:`binary(min_size=n, max_size=n) ` significantly by provide a custom implementation for fixed size blocks that can bypass a lot of machinery. * Set default home directory based on the current working directory at the point Hypothesis is imported, not whenever the function first happens to be called. .. _v3.0.0: ------------------ 3.0.0 - 2016-02-17 ------------------ Codename: This really should have been 2.1. Externally this looks like a very small release. It has one small breaking change that probably doesn't affect anyone at all (some behaviour that never really worked correctly is now outright forbidden) but necessitated a major version bump and one visible new feature. Internally this is a complete rewrite. Almost nothing other than the public API is the same. New features: * Addition of :func:`~hypothesis.strategies.data` strategy which allows you to draw arbitrary data interactively within the test. * New "exploded" database format which allows you to more easily check the example database into a source repository while supporting merging. * Better management of how examples are saved in the database. * Health checks will now raise as errors when they fail. It was too easy to have the warnings be swallowed entirely. New limitations: * ``choices()`` and ``streaming()`` strategies may no longer be used with ``find()``. Neither may :func:`~hypothesis.strategies.data` (this is the change that necessitated a major version bump). Feature removal: * The ForkingTestCase executor has gone away. It may return in some more working form at a later date. Performance improvements: * A new model which allows flatmap, composite strategies and stateful testing to perform *much* better. They should also be more reliable. * Filtering may in some circumstances have improved significantly. This will help especially in cases where you have lots of values with individual filters on them, such as lists(x.filter(...)). * Modest performance improvements to the general test runner by avoiding expensive operations In general your tests should have got faster. If they've instead got significantly slower, I'm interested in hearing about it. Data distribution: The data distribution should have changed significantly. This may uncover bugs the previous version missed. It may also miss bugs the previous version could have uncovered. Hypothesis is now producing less strongly correlated data than it used to, but the correlations are extended over more of the structure. Shrinking: Shrinking quality should have improved. In particular Hypothesis can now perform simultaneous shrinking of separate examples within a single test (previously it was only able to do this for elements of a single collection). In some cases performance will have improved, in some cases it will have got worse but generally shouldn't have by much. Older versions ============== .. _v2.0.0: ------------------ 2.0.0 - 2016-01-10 ------------------ Codename: A new beginning This release cleans up all of the legacy that accrued in the course of Hypothesis 1.0. These are mostly things that were emitting deprecation warnings in 1.19.0, but there were a few additional changes. In particular: * non-strategy values will no longer be converted to strategies when used in given or find. * FailedHealthCheck is now an error and not a warning. * Handling of non-ascii reprs in user types have been simplified by using raw strings in more places in Python 2. * given no longer allows mixing positional and keyword arguments. * given no longer works with functions with defaults. * given no longer turns provided arguments into defaults - they will not appear in the argspec at all. * the basic() strategy no longer exists. * the n_ary_tree strategy no longer exists. * the average_list_length setting no longer exists. Note: If you're using using recursive() this will cause you a significant slow down. You should pass explicit average_size parameters to collections in recursive calls. * @rule can no longer be applied to the same method twice. * Python 2.6 and 3.3 are no longer officially supported, although in practice they still work fine. This also includes two non-deprecation changes: * given's keyword arguments no longer have to be the rightmost arguments and can appear anywhere in the method signature. * The max_shrinks setting would sometimes not have been respected. .. _v1.19.0: ------------------- 1.19.0 - 2016-01-09 ------------------- Codename: IT COMES This release heralds the beginning of a new and terrible age of Hypothesis 2.0. It's primary purpose is some final deprecations prior to said release. The goal is that if your code emits no warnings under this release then it will probably run unchanged under Hypothesis 2.0 (there are some caveats to this: 2.0 will drop support for some Python versions, and if you're using internal APIs then as usual that may break without warning). It does have two new features: * New @seed() decorator which allows you to manually seed a test. This may be harmlessly combined with and overrides the derandomize setting. * settings objects may now be used as a decorator to fix those settings to a particular @given test. API changes (old usage still works but is deprecated): * Settings has been renamed to settings (lower casing) in order to make the decorator usage more natural. * Functions for the storage directory that were in hypothesis.settings are now in a new hypothesis.configuration module. Additional deprecations: * the average_list_length setting has been deprecated in favour of being explicit. * the basic() strategy has been deprecated as it is impossible to support it under a Conjecture based model, which will hopefully be implemented at some point in the 2.x series. * the n_ary_tree strategy (which was never actually part of the public API) has been deprecated. * Passing settings or random as keyword arguments to given is deprecated (use the new functionality instead) Bug fixes: * No longer emit PendingDeprecationWarning for __iter__ and StopIteration in streaming() values. * When running in health check mode with non strict, don't print quite so many errors for an exception in reify. * When an assumption made in a test or a filter is flaky, tests will now raise Flaky instead of UnsatisfiedAssumption. .. _v1.18.1: ------------------- 1.18.1 - 2015-12-22 ------------------- Two behind the scenes changes: * Hypothesis will no longer write generated code to the file system. This will improve performance on some systems (e.g. if you're using `PythonAnywhere `_ which is running your code from NFS) and prevent some annoying interactions with auto-restarting systems. * Hypothesis will cache the creation of some strategies. This can significantly improve performance for code that uses flatmap or composite and thus has to instantiate strategies a lot. .. _v1.18.0: ------------------- 1.18.0 - 2015-12-21 ------------------- Features: * Tests and find are now explicitly seeded off the global random module. This means that if you nest one inside the other you will now get a health check error. It also means that you can control global randomization by seeding random. * There is a new random_module() strategy which seeds the global random module for you and handles things so that you don't get a health check warning if you use it inside your tests. * floats() now accepts two new arguments: allow\_nan and allow\_infinity. These default to the old behaviour, but when set to False will do what the names suggest. Bug fixes: * Fix a bug where tests that used text() on Python 3.4+ would not actually be deterministic even when explicitly seeded or using the derandomize mode, because generation depended on dictionary iteration order which was affected by hash randomization. * Fix a bug where with complicated strategies the timing of the initial health check could affect the seeding of the subsequent test, which would also render supposedly deterministic tests non-deterministic in some scenarios. * In some circumstances flatmap() could get confused by two structurally similar things it could generate and would produce a flaky test where the first time it produced an error but the second time it produced the other value, which was not an error. The same bug was presumably also possible in composite(). * flatmap() and composite() initial generation should now be moderately faster. This will be particularly noticeable when you have many values drawn from the same strategy in a single run, e.g. constructs like lists(s.flatmap(f)). Shrinking performance *may* have suffered, but this didn't actually produce an interestingly worse result in any of the standard scenarios tested. .. _v1.17.1: ------------------- 1.17.1 - 2015-12-16 ------------------- A small bug fix release, which fixes the fact that the 'note' function could not be used on tests which used the @example decorator to provide explicit examples. .. _v1.17.0: ------------------- 1.17.0 - 2015-12-15 ------------------- This is actually the same release as 1.16.1, but 1.16.1 has been pulled because it contains the following additional change that was not intended to be in a patch release (it's perfectly stable, but is a larger change that should have required a minor version bump): * Hypothesis will now perform a series of "health checks" as part of running your tests. These detect and warn about some common error conditions that people often run into which wouldn't necessarily have caused the test to fail but would cause e.g. degraded performance or confusing results. .. _v1.16.1: ------------------- 1.16.1 - 2015-12-14 ------------------- Note: This release has been removed. A small bugfix release that allows bdists for Hypothesis to be built under 2.7 - the compat3.py file which had Python 3 syntax wasn't intended to be loaded under Python 2, but when building a bdist it was. In particular this would break running setup.py test. .. _v1.16.0: ------------------- 1.16.0 - 2015-12-08 ------------------- There are no public API changes in this release but it includes a behaviour change that I wasn't comfortable putting in a patch release. * Functions from hypothesis.strategies will no longer raise InvalidArgument on bad arguments. Instead the same errors will be raised when a test using such a strategy is run. This may improve startup time in some cases, but the main reason for it is so that errors in strategies won't cause errors in loading, and it can interact correctly with things like pytest.mark.skipif. * Errors caused by accidentally invoking the legacy API are now much less confusing, although still throw NotImplementedError. * hypothesis.extra.django is 1.9 compatible. * When tests are run with max_shrinks=0 this will now still rerun the test on failure and will no longer print "Trying example:" before each run. Additionally note() will now work correctly when used with max_shrinks=0. .. _v1.15.0: ------------------- 1.15.0 - 2015-11-24 ------------------- A release with two new features. * A 'characters' strategy for more flexible generation of text with particular character ranges and types, kindly contributed by `Alexander Shorin `_. * Add support for preconditions to the rule based stateful testing. Kindly contributed by `Christopher Armstrong `_ .. _v1.14.0: ------------------- 1.14.0 - 2015-11-01 ------------------- New features: * Add 'note' function which lets you include additional information in the final test run's output. * Add 'choices' strategy which gives you a choice function that emulates random.choice. * Add 'uuid' strategy that generates UUIDs' * Add 'shared' strategy that lets you create a strategy that just generates a single shared value for each test run Bugs: * Using strategies of the form streaming(x.flatmap(f)) with find or in stateful testing would have caused InvalidArgument errors when the resulting values were used (because code that expected to only be called within a test context would be invoked). .. _v1.13.0: ------------------- 1.13.0 - 2015-10-29 ------------------- This is quite a small release, but deprecates some public API functions and removes some internal API functionality so gets a minor version bump. * All calls to the 'strategy' function are now deprecated, even ones which pass just a SearchStrategy instance (which is still a no-op). * Never documented hypothesis.extra entry_points mechanism has now been removed ( it was previously how hypothesis.extra packages were loaded and has been deprecated and unused for some time) * Some corner cases that could previously have produced an OverflowError when simplifying failing cases using hypothesis.extra.datetimes (or dates or times) have now been fixed. * Hypothesis load time for first import has been significantly reduced - it used to be around 250ms (on my SSD laptop) and now is around 100-150ms. This almost never matters but was slightly annoying when using it in the console. * hypothesis.strategies.randoms was previously missing from \_\_all\_\_. .. _v1.12.0: ------------------- 1.12.0 - 2015-10-18 ------------------- * Significantly improved performance of creating strategies using the functions from the hypothesis.strategies module by deferring the calculation of their repr until it was needed. This is unlikely to have been an performance issue for you unless you were using flatmap, composite or stateful testing, but for some cases it could be quite a significant impact. * A number of cases where the repr of strategies build from lambdas is improved * Add dates() and times() strategies to hypothesis.extra.datetimes * Add new 'profiles' mechanism to the settings system * Deprecates mutability of Settings, both the Settings.default top level property and individual settings. * A Settings object may now be directly initialized from a parent Settings. * @given should now give a better error message if you attempt to use it with a function that uses destructuring arguments (it still won't work, but it will error more clearly), * A number of spelling corrections in error messages * :pypi:`pytest` should no longer display the intermediate modules Hypothesis generates when running in verbose mode * Hypothesis should now correctly handle printing objects with non-ascii reprs on python 3 when running in a locale that cannot handle ascii printing to stdout. * Add a unique=True argument to lists(). This is equivalent to unique_by=lambda x: x, but offers a more convenient syntax. .. _v1.11.4: ------------------- 1.11.4 - 2015-09-27 ------------------- * Hide modifications Hypothesis needs to make to sys.path by undoing them after we've imported the relevant modules. This is a workaround for issues cryptography experienced on windows. * Slightly improved performance of drawing from sampled_from on large lists of alternatives. * Significantly improved performance of drawing from one_of or strategies using \| (note this includes a lot of strategies internally - floats() and integers() both fall into this category). There turned out to be a massive performance regression introduced in 1.10.0 affecting these which probably would have made tests using Hypothesis significantly slower than they should have been. .. _v1.11.3: ------------------- 1.11.3 - 2015-09-23 ------------------- * Better argument validation for datetimes() strategy - previously setting max_year < datetime.MIN_YEAR or min_year > datetime.MAX_YEAR would not have raised an InvalidArgument error and instead would have behaved confusingly. * Compatibility with being run on pytest < 2.7 (achieved by disabling the plugin). .. _v1.11.2: ------------------- 1.11.2 - 2015-09-23 ------------------- Bug fixes: * Settings(database=my_db) would not be correctly inherited when used as a default setting, so that newly created settings would use the database_file setting and create an SQLite example database. * Settings.default.database = my_db would previously have raised an error and now works. * Timeout could sometimes be significantly exceeded if during simplification there were a lot of examples tried that didn't trigger the bug. * When loading a heavily simplified example using a basic() strategy from the database this could cause Python to trigger a recursion error. * Remove use of deprecated API in pytest plugin so as to not emit warning Misc: * hypothesis-pytest is now part of hypothesis core. This should have no externally visible consequences, but you should update your dependencies to remove hypothesis-pytest and depend on only Hypothesis. * Better repr for hypothesis.extra.datetimes() strategies. * Add .close() method to abstract base class for Backend (it was already present in the main implementation). .. _v1.11.1: ------------------- 1.11.1 - 2015-09-16 ------------------- Bug fixes: * When running Hypothesis tests in parallel (e.g. using pytest-xdist) there was a race condition caused by code generation. * Example databases are now cached per thread so as to not use sqlite connections from multiple threads. This should make Hypothesis now entirely thread safe. * floats() with only min_value or max_value set would have had a very bad distribution. * Running on 3.5, Hypothesis would have emitted deprecation warnings because of use of inspect.getargspec .. _v1.11.0: ------------------- 1.11.0 - 2015-08-31 ------------------- * text() with a non-string alphabet would have used the repr() of the alphabet instead of its contexts. This is obviously silly. It now works with any sequence of things convertible to unicode strings. * @given will now work on methods whose definitions contains no explicit positional arguments, only varargs (:issue:`118`). This may have some knock on effects because it means that @given no longer changes the argspec of functions other than by adding defaults. * Introduction of new @composite feature for more natural definition of strategies you'd previously have used flatmap for. .. _v1.10.6: ------------------- 1.10.6 - 2015-08-26 ------------------- Fix support for fixtures on Django 1.7. .. _v1.10.4: ------------------- 1.10.4 - 2015-08-21 ------------------- Tiny bug fix release: * If the database_file setting is set to None, this would have resulted in an error when running tests. Now it does the same as setting database to None. .. _v1.10.3: ------------------- 1.10.3 - 2015-08-19 ------------------- Another small bug fix release. * lists(elements, unique_by=some_function, min_size=n) would have raised a ValidationError if n > Settings.default.average_list_length because it would have wanted to use an average list length shorter than the minimum size of the list, which is impossible. Now it instead defaults to twice the minimum size in these circumstances. * basic() strategy would have only ever produced at most ten distinct values per run of the test (which is bad if you e.g. have it inside a list). This was obviously silly. It will now produce a much better distribution of data, both duplicated and non duplicated. .. _v1.10.2: ------------------- 1.10.2 - 2015-08-19 ------------------- This is a small bug fix release: * star imports from hypothesis should now work correctly. * example quality for examples using flatmap will be better, as the way it had previously been implemented was causing problems where Hypothesis was erroneously labelling some examples as being duplicates. .. _v1.10.0: ------------------- 1.10.0 - 2015-08-04 ------------------- This is just a bugfix and performance release, but it changes some semi-public APIs, hence the minor version bump. * Significant performance improvements for strategies which are one\_of() many branches. In particular this included recursive() strategies. This should take the case where you use one recursive() strategy as the base strategy of another from unusably slow (tens of seconds per generated example) to reasonably fast. * Better handling of just() and sampled_from() for values which have an incorrect \_\_repr\_\_ implementation that returns non-ASCII unicode on Python 2. * Better performance for flatmap from changing the internal morpher API to be significantly less general purpose. * Introduce a new semi-public BuildContext/cleanup API. This allows strategies to register cleanup activities that should run once the example is complete. Note that this will interact somewhat weirdly with find. * Better simplification behaviour for streaming strategies. * Don't error on lambdas which use destructuring arguments in Python 2. * Add some better reprs for a few strategies that were missing good ones. * The Random instances provided by randoms() are now copyable. * Slightly more debugging information about simplify when using a debug verbosity level. * Support using given for functions with varargs, but not passing arguments to it as positional. .. _v1.9.0: ------------------ 1.9.0 - 2015-07-27 ------------------ Codename: The great bundling. This release contains two fairly major changes. The first is the deprecation of the hypothesis-extra mechanism. From now on all the packages that were previously bundled under it other than hypothesis-pytest (which is a different beast and will remain separate). The functionality remains unchanged and you can still import them from exactly the same location, they just are no longer separate packages. The second is that this introduces a new way of building strategies which lets you build up strategies recursively from other strategies. It also contains the minor change that calling .example() on a strategy object will give you examples that are more representative of the actual data you'll get. There used to be some logic in there to make the examples artificially simple but this proved to be a bad idea. .. _v1.8.5: ------------------ 1.8.5 - 2015-07-24 ------------------ This contains no functionality changes but fixes a mistake made with building the previous package that would have broken installation on Windows. .. _v1.8.4: ------------------ 1.8.4 - 2015-07-20 ------------------ Bugs fixed: * When a call to floats() had endpoints which were not floats but merely convertible to one (e.g. integers), these would be included in the generated data which would cause it to generate non-floats. * Splitting lambdas used in the definition of flatmap, map or filter over multiple lines would break the repr, which would in turn break their usage. .. _v1.8.3: ------------------ 1.8.3 - 2015-07-20 ------------------ "Falsifying example" would not have been printed when the failure came from an explicit example. .. _v1.8.2: ------------------ 1.8.2 - 2015-07-18 ------------------ Another small bugfix release: * When using ForkingTestCase you would usually not get the falsifying example printed if the process exited abnormally (e.g. due to os._exit). * Improvements to the distribution of characters when using text() with a default alphabet. In particular produces a better distribution of ascii and whitespace in the alphabet. .. _v1.8.1: ------------------ 1.8.1 - 2015-07-17 ------------------ This is a small release that contains a workaround for people who have bad reprs returning non ascii text on Python 2.7. This is not a bug fix for Hypothesis per se because that's not a thing that is actually supposed to work, but Hypothesis leans more heavily on repr than is typical so it's worth having a workaround for. .. _v1.8.0: ------------------ 1.8.0 - 2015-07-16 ------------------ New features: * Much more sensible reprs for strategies, especially ones that come from hypothesis.strategies. These should now have as reprs python code that would produce the same strategy. * lists() accepts a unique_by argument which forces the generated lists to be only contain elements unique according to some function key (which must return a hashable value). * Better error messages from flaky tests to help you debug things. Mostly invisible implementation details that may result in finding new bugs in your code: * Sets and dictionary generation should now produce a better range of results. * floats with bounds now focus more on 'critical values', trying to produce values at edge cases. * flatmap should now have better simplification for complicated cases, as well as generally being (I hope) more reliable. Bug fixes: * You could not previously use assume() if you were using the forking executor. .. _v1.7.2: ------------------ 1.7.2 - 2015-07-10 ------------------ This is purely a bug fix release: * When using floats() with stale data in the database you could sometimes get values in your tests that did not respect min_value or max_value. * When getting a Flaky error from an unreliable test it would have incorrectly displayed the example that caused it. * 2.6 dependency on backports was incorrectly specified. This would only have caused you problems if you were building a universal wheel from Hypothesis, which is not how Hypothesis ships, so unless you're explicitly building wheels for your dependencies and support Python 2.6 plus a later version of Python this probably would never have affected you. * If you use flatmap in a way that the strategy on the right hand side depends sensitively on the left hand side you may have occasionally seen Flaky errors caused by producing unreliable examples when minimizing a bug. This use case may still be somewhat fraught to be honest. This code is due a major rearchitecture for 1.8, but in the meantime this release fixes the only source of this error that I'm aware of. .. _v1.7.1: ------------------ 1.7.1 - 2015-06-29 ------------------ Codename: There is no 1.7.0. A slight technical hitch with a premature upload means there's was a yanked 1.7.0 release. Oops. The major feature of this release is Python 2.6 support. Thanks to Jeff Meadows for doing most of the work there. Other minor features * strategies now has a permutations() function which returns a strategy yielding permutations of values from a given collection. * if you have a flaky test it will print the exception that it last saw before failing with Flaky, even if you do not have verbose reporting on. * Slightly experimental git merge script available as "python -m hypothesis.tools.mergedbs". Instructions on how to use it in the docstring of that file. Bug fixes: * Better performance from use of filter. In particular tests which involve large numbers of heavily filtered strategies should perform a lot better. * floats() with a negative min_value would not have worked correctly (worryingly, it would have just silently failed to run any examples). This is now fixed. * tests using sampled\_from would error if the number of sampled elements was smaller than min\_satisfying\_examples. .. _v1.6.2: ------------------ 1.6.2 - 2015-06-08 ------------------ This is just a few small bug fixes: * Size bounds were not validated for values for a binary() strategy when reading examples from the database. * sampled\_from is now in __all__ in hypothesis.strategies * floats no longer consider negative integers to be simpler than positive non-integers * Small floating point intervals now correctly count members, so if you have a floating point interval so narrow there are only a handful of values in it, this will no longer cause an error when Hypothesis runs out of values. .. _v1.6.1: ------------------ 1.6.1 - 2015-05-21 ------------------ This is a small patch release that fixes a bug where 1.6.0 broke the use of flatmap with the deprecated API and assumed the passed in function returned a SearchStrategy instance rather than converting it to a strategy. .. _v1.6.0: ------------------ 1.6.0 - 2015-05-21 ------------------ This is a smallish release designed to fix a number of bugs and smooth out some weird behaviours. * Fix a critical bug in flatmap where it would reuse old strategies. If all your flatmap code was pure you're fine. If it's not, I'm surprised it's working at all. In particular if you want to use flatmap with django models, you desperately need to upgrade to this version. * flatmap simplification performance should now be better in some cases where it previously had to redo work. * Fix for a bug where invalid unicode data with surrogates could be generated during simplification (it was already filtered out during actual generation). * The Hypothesis database is now keyed off the name of the test instead of the type of data. This makes much more sense now with the new strategies API and is generally more robust. This means you will lose old examples on upgrade. * The database will now not delete values which fail to deserialize correctly, just skip them. This is to handle cases where multiple incompatible strategies share the same key. * find now also saves and loads values from the database, keyed off a hash of the function you're finding from. * Stateful tests now serialize and load values from the database. They should have before, really. This was a bug. * Passing a different verbosity level into a test would not have worked entirely correctly, leaving off some messages. This is now fixed. * Fix a bug where derandomized tests with unicode characters in the function body would error on Python 2.7. .. _v1.5.0: ------------------ 1.5.0 - 2015-05-14 ------------------ Codename: Strategic withdrawal. The purpose of this release is a radical simplification of the API for building strategies. Instead of the old approach of @strategy.extend and things that get converted to strategies, you just build strategies directly. The old method of defining strategies will still work until Hypothesis 2.0, because it's a major breaking change, but will now emit deprecation warnings. The new API is also a lot more powerful as the functions for defining strategies give you a lot of dials to turn. See :doc:`the updated data section ` for details. Other changes: * Mixing keyword and positional arguments in a call to @given is deprecated as well. * There is a new setting called 'strict'. When set to True, Hypothesis will raise warnings instead of merely printing them. Turning it on by default is inadvisable because it means that Hypothesis minor releases can break your code, but it may be useful for making sure you catch all uses of deprecated APIs. * max_examples in settings is now interpreted as meaning the maximum number of unique (ish) examples satisfying assumptions. A new setting max_iterations which defaults to a larger value has the old interpretation. * Example generation should be significantly faster due to a new faster parameter selection algorithm. This will mostly show up for simple data types - for complex ones the parameter selection is almost certainly dominated. * Simplification has some new heuristics that will tend to cut down on cases where it could previously take a very long time. * timeout would previously not have been respected in cases where there were a lot of duplicate examples. You probably wouldn't have previously noticed this because max_examples counted duplicates, so this was very hard to hit in a way that mattered. * A number of internal simplifications to the SearchStrategy API. * You can now access the current Hypothesis version as hypothesis.__version__. * A top level function is provided for running the stateful tests without the TestCase infrastructure. .. _v1.4.0: ------------------ 1.4.0 - 2015-05-04 ------------------ Codename: What a state. The *big* feature of this release is the new and slightly experimental stateful testing API. You can read more about that in :ref:`the appropriate section `. Two minor features the were driven out in the course of developing this: * You can now set settings.max_shrinks to limit the number of times Hypothesis will try to shrink arguments to your test. If this is set to <= 0 then Hypothesis will not rerun your test and will just raise the failure directly. Note that due to technical limitations if max_shrinks is <= 0 then Hypothesis will print *every* example it calls your test with rather than just the failing one. Note also that I don't consider settings max_shrinks to zero a sensible way to run your tests and it should really be considered a debug feature. * There is a new debug level of verbosity which is even *more* verbose than verbose. You probably don't want this. Breakage of semi-public SearchStrategy API: * It is now a required invariant of SearchStrategy that if u simplifies to v then it is not the case that strictly_simpler(u, v). i.e. simplifying should not *increase* the complexity even though it is not required to decrease it. Enforcing this invariant lead to finding some bugs where simplifying of integers, floats and sets was suboptimal. * Integers in basic data are now required to fit into 64 bits. As a result python integer types are now serialized as strings, and some types have stopped using quite so needlessly large random seeds. Hypothesis Stateful testing was then turned upon Hypothesis itself, which lead to an amazing number of minor bugs being found in Hypothesis itself. Bugs fixed (most but not all from the result of stateful testing) include: * Serialization of streaming examples was flaky in a way that you would probably never notice: If you generate a template, simplify it, serialize it, deserialize it, serialize it again and then deserialize it you would get the original stream instead of the simplified one. * If you reduced max_examples below the number of examples already saved in the database, you would have got a ValueError. Additionally, if you had more than max_examples in the database all of them would have been considered. * @given will no longer count duplicate examples (which it never called your function with) towards max_examples. This may result in your tests running slower, but that's probably just because they're trying more examples. * General improvements to example search which should result in better performance and higher quality examples. In particular parameters which have a history of producing useless results will be more aggressively culled. This is useful both because it decreases the chance of useless examples and also because it's much faster to not check parameters which we were unlikely to ever pick! * integers_from and lists of types with only one value (e.g. [None]) would previously have had a very high duplication rate so you were probably only getting a handful of examples. They now have a much lower duplication rate, as well as the improvements to search making this less of a problem in the first place. * You would sometimes see simplification taking significantly longer than your defined timeout. This would happen because timeout was only being checked after each *successful* simplification, so if Hypothesis was spending a lot of time unsuccessfully simplifying things it wouldn't stop in time. The timeout is now applied for unsuccessful simplifications too. * In Python 2.7, integers_from strategies would have failed during simplification with an OverflowError if their starting point was at or near to the maximum size of a 64-bit integer. * flatmap and map would have failed if called with a function without a __name__ attribute. * If max_examples was less than min_satisfying_examples this would always error. Now min_satisfying_examples is capped to max_examples. Note that if you have assumptions to satisfy here this will still cause an error. Some minor quality improvements: * Lists of streams, flatmapped strategies and basic strategies should now now have slightly better simplification. .. _v1.3.0: ------------------ 1.3.0 - 2015-05-22 ------------------ New features: * New verbosity level API for printing intermediate results and exceptions. * New specifier for strings generated from a specified alphabet. * Better error messages for tests that are failing because of a lack of enough examples. Bug fixes: * Fix error where use of ForkingTestCase would sometimes result in too many open files. * Fix error where saving a failing example that used flatmap could error. * Implement simplification for sampled_from, which apparently never supported it previously. Oops. General improvements: * Better range of examples when using one_of or sampled_from. * Fix some pathological performance issues when simplifying lists of complex values. * Fix some pathological performance issues when simplifying examples that require unicode strings with high codepoints. * Random will now simplify to more readable examples. .. _v1.2.1: ------------------ 1.2.1 - 2015-04-16 ------------------ A small patch release for a bug in the new executors feature. Tests which require doing something to their result in order to fail would have instead reported as flaky. .. _v1.2.0: ------------------ 1.2.0 - 2015-04-15 ------------------ Codename: Finders keepers. A bunch of new features and improvements. * Provide a mechanism for customizing how your tests are executed. * Provide a test runner that forks before running each example. This allows better support for testing native code which might trigger a segfault or a C level assertion failure. * Support for using Hypothesis to find examples directly rather than as just as a test runner. * New streaming type which lets you generate infinite lazily loaded streams of data - perfect for if you need a number of examples but don't know how many. * Better support for large integer ranges. You can now use integers_in_range with ranges of basically any size. Previously large ranges would have eaten up all your memory and taken forever. * Integers produce a wider range of data than before - previously they would only rarely produce integers which didn't fit into a machine word. Now it's much more common. This percolates to other numeric types which build on integers. * Better validation of arguments to @given. Some situations that would previously have caused silently wrong behaviour will now raise an error. * Include +/- sys.float_info.max in the set of floating point edge cases that Hypothesis specifically tries. * Fix some bugs in floating point ranges which happen when given +/- sys.float_info.max as one of the endpoints... (really any two floats that are sufficiently far apart so that x, y are finite but y - x is infinite). This would have resulted in generating infinite values instead of ones inside the range. .. _v1.1.1: ------------------ 1.1.1 - 2015-04-07 ------------------ Codename: Nothing to see here This is just a patch release put out because it fixed some internal bugs that would block the Django integration release but did not actually affect anything anyone could previously have been using. It also contained a minor quality fix for floats that I'd happened to have finished in time. * Fix some internal bugs with object lifecycle management that were impossible to hit with the previously released versions but broke hypothesis-django. * Bias floating point numbers somewhat less aggressively towards very small numbers .. _v1.1.0: ------------------ 1.1.0 - 2015-04-06 ------------------ Codename: No-one mention the M word. * Unicode strings are more strongly biased towards ascii characters. Previously they would generate all over the space. This is mostly so that people who try to shape their unicode strings with assume() have less of a bad time. * A number of fixes to data deserialization code that could theoretically have caused mysterious bugs when using an old version of a Hypothesis example database with a newer version. To the best of my knowledge a change that could have triggered this bug has never actually been seen in the wild. Certainly no-one ever reported a bug of this nature. * Out of the box support for Decimal and Fraction. * new dictionary specifier for dictionaries with variable keys. * Significantly faster and higher quality simplification, especially for collections of data. * New filter() and flatmap() methods on Strategy for better ways of building strategies out of other strategies. * New BasicStrategy class which allows you to define your own strategies from scratch without needing an existing matching strategy or being exposed to the full horror or non-public nature of the SearchStrategy interface. .. _v1.0.0: ------------------ 1.0.0 - 2015-03-27 ------------------ Codename: Blast-off! There are no code changes in this release. This is precisely the 0.9.2 release with some updated documentation. .. _v0.9.2: ------------------ 0.9.2 - 2015-03-26 ------------------ Codename: T-1 days. * floats_in_range would not actually have produced floats_in_range unless that range happened to be (0, 1). Fix this. .. _v0.9.1: ------------------ 0.9.1 - 2015-03-25 ------------------ Codename: T-2 days. * Fix a bug where if you defined a strategy using map on a lambda then the results would not be saved in the database. * Significant performance improvements when simplifying examples using lists, strings or bounded integer ranges. .. _v0.9.0: ------------------ 0.9.0 - 2015-03-23 ------------------ Codename: The final countdown This release could also be called 1.0-RC1. It contains a teeny tiny bugfix, but the real point of this release is to declare feature freeze. There will be zero functionality changes between 0.9.0 and 1.0 unless something goes really really wrong. No new features will be added, no breaking API changes will occur, etc. This is the final shakedown before I declare Hypothesis stable and ready to use and throw a party to celebrate. Bug bounty for any bugs found between now and 1.0: I will buy you a drink (alcoholic, caffeinated, or otherwise) and shake your hand should we ever find ourselves in the same city at the same time. The one tiny bugfix: * Under pypy, databases would fail to close correctly when garbage collected, leading to a memory leak and a confusing error message if you were repeatedly creating databases and not closing them. It is very unlikely you were doing this and the chances of you ever having noticed this bug are very low. .. _v0.7.2: ------------------ 0.7.2 - 2015-03-22 ------------------ Codename: Hygienic macros or bust * You can now name an argument to @given 'f' and it won't break (:issue:`38`) * strategy_test_suite is now named strategy_test_suite as the documentation claims and not in fact strategy_test_suitee * Settings objects can now be used as a context manager to temporarily override the default values inside their context. .. _v0.7.1: ------------------ 0.7.1 - 2015-03-21 ------------------ Codename: Point releases go faster * Better string generation by parametrizing by a limited alphabet * Faster string simplification - previously if simplifying a string with high range unicode characters it would try every unicode character smaller than that. This was pretty pointless. Now it stops after it's a short range (it can still reach smaller ones through recursive calls because of other simplifying operations). * Faster list simplification by first trying a binary chop down the middle * Simultaneous simplification of identical elements in a list. So if a bug only triggers when you have duplicates but you drew e.g. [-17, -17], this will now simplify to [0, 0]. .. _v0.7.0,: ------------------- 0.7.0, - 2015-03-20 ------------------- Codename: Starting to look suspiciously real This is probably the last minor release prior to 1.0. It consists of stability improvements, a few usability things designed to make Hypothesis easier to try out, and filing off some final rough edges from the API. * Significant speed and memory usage improvements * Add an example() method to strategy objects to give an example of the sort of data that the strategy generates. * Remove .descriptor attribute of strategies * Rename descriptor_test_suite to strategy_test_suite * Rename the few remaining uses of descriptor to specifier (descriptor already has a defined meaning in Python) .. _v0.6.0: --------------------------------------------------------- 0.6.0 - 2015-03-13 --------------------------------------------------------- Codename: I'm sorry, were you using that API? This is primarily a "simplify all the weird bits of the API" release. As a result there are a lot of breaking changes. If you just use @given with core types then you're probably fine. In particular: * Stateful testing has been removed from the API * The way the database is used has been rendered less useful (sorry). The feature for reassembling values saved from other tests doesn't currently work. This will probably be brought back in post 1.0. * SpecificationMapper is no longer a thing. Instead there is an ExtMethod called strategy which you extend to specify how to convert other types to strategies. * Settings are now extensible so you can add your own for configuring a strategy * MappedSearchStrategy no longer needs an unpack method * Basically all the SearchStrategy internals have changed massively. If you implemented SearchStrategy directly rather than using MappedSearchStrategy talk to me about fixing it. * Change to the way extra packages work. You now specify the package. This must have a load() method. Additionally any modules in the package will be loaded in under hypothesis.extra Bug fixes: * Fix for a bug where calling falsify on a lambda with a non-ascii character in its body would error. Hypothesis Extra: hypothesis-fakefactory\: An extension for using faker data in hypothesis. Depends on fake-factory. .. _v0.5.0: ------------------ 0.5.0 - 2015-02-10 ------------------ Codename: Read all about it. Core hypothesis: * Add support back in for pypy and python 3.2 * @given functions can now be invoked with some arguments explicitly provided. If all arguments that hypothesis would have provided are passed in then no falsification is run. * Related to the above, this means that you can now use pytest fixtures and mark.parametrize with Hypothesis without either interfering with the other. * Breaking change: @given no longer works for functions with varargs (varkwargs are fine). This might be added back in at a later date. * Windows is now fully supported. A limited version (just the tests with none of the extras) of the test suite is run on windows with each commit so it is now a first class citizen of the Hypothesis world. * Fix a bug for fuzzy equality of equal complex numbers with different reprs (this can happen when one coordinate is zero). This shouldn't affect users - that feature isn't used anywhere public facing. * Fix generation of floats on windows and 32-bit builds of python. I was using some struct.pack logic that only worked on certain word sizes. * When a test times out and hasn't produced enough examples this now raises a Timeout subclass of Unfalsifiable. * Small search spaces are better supported. Previously something like a @given(bool, bool) would have failed because it couldn't find enough examples. Hypothesis is now aware of the fact that these are small search spaces and will not error in this case. * Improvements to parameter search in the case of hard to satisfy assume. Hypothesis will now spend less time exploring parameters that are unlikely to provide anything useful. * Increase chance of generating "nasty" floats * Fix a bug that would have caused unicode warnings if you had a sampled_from that was mixing unicode and byte strings. * Added a standard test suite that you can use to validate a custom strategy you've defined is working correctly. Hypothesis extra: First off, introducing Hypothesis extra packages! These are packages that are separated out from core Hypothesis because they have one or more dependencies. Every hypothesis-extra package is pinned to a specific point release of Hypothesis and will have some version requirements on its dependency. They use entry_points so you will usually not need to explicitly import them, just have them installed on the path. This release introduces two of them: hypothesis-datetime: Does what it says on the tin: Generates datetimes for Hypothesis. Just install the package and datetime support will start working. Depends on pytz for timezone support hypothesis-pytest: A very rudimentary pytest plugin. All it does right now is hook the display of falsifying examples into pytest reporting. Depends on pytest. .. _v0.4.3: ------------------ 0.4.3 - 2015-02-05 ------------------ Codename: TIL narrow Python builds are a thing This just fixes the one bug. * Apparently there is such a thing as a "narrow python build" and OS X ships with these by default for python 2.7. These are builds where you only have two bytes worth of unicode. As a result, generating unicode was completely broken on OS X. Fix this by only generating unicode codepoints in the range supported by the system. .. _v0.4.2: ------------------ 0.4.2 - 2015-02-04 ------------------ Codename: O(dear) This is purely a bugfix release: * Provide sensible external hashing for all core types. This will significantly improve performance of tracking seen examples which happens in literally every falsification run. For Hypothesis fixing this cut 40% off the runtime of the test suite. The behaviour is quadratic in the number of examples so if you're running the default configuration this will be less extreme (Hypothesis's test suite runs at a higher number of examples than default), but you should still see a significant improvement. * Fix a bug in formatting of complex numbers where the string could get incorrectly truncated. .. _v0.4.1: ------------------ 0.4.1 - 2015-02-03 ------------------ Codename: Cruel and unusual edge cases This release is mostly about better test case generation. Enhancements: * Has a cool release name * text_type (str in python 3, unicode in python 2) example generation now actually produces interesting unicode instead of boring ascii strings. * floating point numbers are generated over a much wider range, with particular attention paid to generating nasty numbers - nan, infinity, large and small values, etc. * examples can be generated using pieces of examples previously saved in the database. This allows interesting behaviour that has previously been discovered to be propagated to other examples. * improved parameter exploration algorithm which should allow it to more reliably hit interesting edge cases. * Timeout can now be disabled entirely by setting it to any value <= 0. Bug fixes: * The descriptor on a OneOfStrategy could be wrong if you had descriptors which were equal but should not be coalesced. e.g. a strategy for one_of((frozenset({int}), {int})) would have reported its descriptor as {int}. This is unlikely to have caused you any problems * If you had strategies that could produce NaN (which float previously couldn't but e.g. a Just(float('nan')) could) then this would have sent hypothesis into an infinite loop that would have only been terminated when it hit the timeout. * Given elements that can take a long time to minimize, minimization of floats or tuples could be quadratic or worse in the that value. You should now see much better performance for simplification, albeit at some cost in quality. Other: * A lot of internals have been rewritten. This shouldn't affect you at all, but it opens the way for certain of hypothesis's oddities to be a lot more extensible by users. Whether this is a good thing may be up for debate... .. _v0.4.0: ------------------ 0.4.0 - 2015-01-21 ------------------ FLAGSHIP FEATURE: Hypothesis now persists examples for later use. It stores data in a local SQLite database and will reuse it for all tests of the same type. LICENSING CHANGE: Hypothesis is now released under the Mozilla Public License 2.0. This applies to all versions from 0.4.0 onwards until further notice. The previous license remains applicable to all code prior to 0.4.0. Enhancements: * Printing of failing examples. I was finding that the pytest runner was not doing a good job of displaying these, and that Hypothesis itself could do much better. * Drop dependency on six for cross-version compatibility. It was easy enough to write the shim for the small set of features that we care about and this lets us avoid a moderately complex dependency. * Some improvements to statistical distribution of selecting from small (<= 3 elements) * Improvements to parameter selection for finding examples. Bugs fixed: * could_have_produced for lists, dicts and other collections would not have examined the elements and thus when using a union of different types of list this could result in Hypothesis getting confused and passing a value to the wrong strategy. This could potentially result in exceptions being thrown from within simplification. * sampled_from would not work correctly on a single element list. * Hypothesis could get *very* confused by values which are equal despite having different types being used in descriptors. Hypothesis now has its own more specific version of equality it uses for descriptors and tracking. It is always more fine grained than Python equality: Things considered != are not considered equal by hypothesis, but some things that are considered == are distinguished. If your test suite uses both frozenset and set tests this bug is probably affecting you. .. _v0.3.2: ------------------ 0.3.2 - 2015-01-16 ------------------ * Fix a bug where if you specified floats_in_range with integer arguments Hypothesis would error in example simplification. * Improve the statistical distribution of the floats you get for the floats_in_range strategy. I'm not sure whether this will affect users in practice but it took my tests for various conditions from flaky to rock solid so it at the very least improves discovery of the artificial cases I'm looking for. * Improved repr() for strategies and RandomWithSeed instances. * Add detection for flaky test cases where hypothesis managed to find an example which breaks it but on the final invocation of the test it does not raise an error. This will typically happen with too much recursion errors but could conceivably happen in other circumstances too. * Provide a "derandomized" mode. This allows you to run hypothesis with zero real randomization, making your build nice and deterministic. The tests run with a seed calculated from the function they're testing so you should still get a good distribution of test cases. * Add a mechanism for more conveniently defining tests which just sample from some collection. * Fix for a really subtle bug deep in the internals of the strategy table. In some circumstances if you were to define instance strategies for both a parent class and one or more of its subclasses you would under some circumstances get the strategy for the wrong superclass of an instance. It is very unlikely anyone has ever encountered this in the wild, but it is conceivably possible given that a mix of namedtuple and tuple are used fairly extensively inside hypothesis which do exhibit this pattern of strategy. .. _v0.3.1: ------------------ 0.3.1 - 2015-01-13 ------------------ * Support for generation of frozenset and Random values * Correct handling of the case where a called function mutates it argument. This involved introducing a notion of a strategies knowing how to copy their argument. The default method should be entirely acceptable and the worst case is that it will continue to have the old behaviour if you don't mark your strategy as mutable, so this shouldn't break anything. * Fix for a bug where some strategies did not correctly implement could_have_produced. It is very unlikely that any of these would have been seen in the wild, and the consequences if they had been would have been minor. * Re-export the @given decorator from the main hypothesis namespace. It's still available at the old location too. * Minor performance optimisation for simplifying long lists. .. _v0.3.0: ------------------ 0.3.0 - 2015-01-12 ------------------ * Complete redesign of the data generation system. Extreme breaking change for anyone who was previously writing their own SearchStrategy implementations. These will not work any more and you'll need to modify them. * New settings system allowing more global and modular control of Verifier behaviour. * Decouple SearchStrategy from the StrategyTable. This leads to much more composable code which is a lot easier to understand. * A significant amount of internal API renaming and moving. This may also break your code. * Expanded available descriptors, allowing for generating integers or floats in a specific range. * Significantly more robust. A very large number of small bug fixes, none of which anyone is likely to have ever noticed. * Deprecation of support for pypy and python 3 prior to 3.3. 3.3 and 3.4. Supported versions are 2.7.x, 3.3.x, 3.4.x. I expect all of these to remain officially supported for a very long time. I would not be surprised to add pypy support back in later but I'm not going to do so until I know someone cares about it. In the meantime it will probably still work. .. _v0.2.2: ------------------ 0.2.2 - 2015-01-08 ------------------ * Fix an embarrassing complete failure of the installer caused by my being bad at version control .. _v0.2.1: ------------------ 0.2.1 - 2015-01-07 ------------------ * Fix a bug in the new stateful testing feature where you could make __init__ a @requires method. Simplification would not always work if the prune method was able to successfully shrink the test. .. _v0.2.0: ------------------ 0.2.0 - 2015-01-07 ------------------ * It's aliiive. * Improve python 3 support using six. * Distinguish between byte and unicode types. * Fix issues where FloatStrategy could raise. * Allow stateful testing to request constructor args. * Fix for issue where test annotations would timeout based on when the module was loaded instead of when the test started .. _v0.1.4: ------------------ 0.1.4 - 2013-12-14 ------------------ * Make verification runs time bounded with a configurable timeout .. _v0.1.3: ------------------ 0.1.3 - 2013-05-03 ------------------ * Bugfix: Stateful testing behaved incorrectly with subclassing. * Complex number support * support for recursive strategies * different error for hypotheses with unsatisfiable assumptions .. _v0.1.2: ------------------ 0.1.2 - 2013-03-24 ------------------ * Bugfix: Stateful testing was not minimizing correctly and could throw exceptions. * Better support for recursive strategies. * Support for named tuples. * Much faster integer generation. .. _v0.1.1: ------------------ 0.1.1 - 2013-03-24 ------------------ * Python 3.x support via 2to3. * Use new style classes (oops). .. _v0.1.0: ------------------ 0.1.0 - 2013-03-23 ------------------ * Introduce stateful testing. * Massive rewrite of internals to add flags and strategies. .. _v0.0.5: ------------------ 0.0.5 - 2013-03-13 ------------------ * No changes except trying to fix packaging .. _v0.0.4: ------------------ 0.0.4 - 2013-03-13 ------------------ * No changes except that I checked in a failing test case for 0.0.3 so had to replace the release. Doh .. _v0.0.3: ------------------ 0.0.3 - 2013-03-13 ------------------ * Improved a few internals. * Opened up creating generators from instances as a general API. * Test integration. .. _v0.0.2: ------------------ 0.0.2 - 2013-03-12 ------------------ * Starting to tighten up on the internals. * Change API to allow more flexibility in configuration. * More testing. .. _v0.0.1: ------------------ 0.0.1 - 2013-03-10 ------------------ * Initial release. * Basic working prototype. Demonstrates idea, probably shouldn't be used. ================================================ FILE: hypothesis-python/docs/community.rst ================================================ ========= Community ========= The Hypothesis community is full of excellent people who can answer your questions and help you out. Please do join us: * You can post a question on Stack Overflow using the `python-hypothesis `__ tag. * We also have `a mailing list `_. Feel free to use it to ask for help, provide feedback, or discuss anything remotely Hypothesis related at all. Note that the :gh-file:`Hypothesis code of conduct <.github/CODE_OF_CONDUCT.rst>` applies in all Hypothesis community spaces! If you would like to cite Hypothesis, please consider :gh-file:`our suggested citation `. If you like repo badges, we suggest the following badge, which you can add with Markdown or reStructuredText respectively: .. image:: https://img.shields.io/badge/hypothesis-tested-brightgreen.svg .. code:: md [![Tested with Hypothesis](https://img.shields.io/badge/hypothesis-tested-brightgreen.svg)](https://hypothesis.readthedocs.io/) .. code:: restructuredtext .. image:: https://img.shields.io/badge/hypothesis-tested-brightgreen.svg :alt: Tested with Hypothesis :target: https://hypothesis.readthedocs.io Finally, we have a beautiful logo which appears online, and often on stickers: .. image:: ../../brand/dragonfly-rainbow.svg :alt: The Hypothesis logo, a dragonfly with rainbow wings :align: center :width: 50 % As well as being beautiful, dragonflies actively hunt down bugs for a living! You can find the images and a usage guide in the :gh-file:`brand` directory on GitHub, or find us at conferences where we often have stickers and sometimes other swag. ================================================ FILE: hypothesis-python/docs/compatibility.rst ================================================ Compatibility ============= Hypothesis generally does its level best to be compatible with everything you could need it to be compatible with. This document outlines our compatibility status and guarantees. Hypothesis versions ------------------- Backwards compatibility is better than backporting fixes, so we use :ref:`semantic versioning ` and only support the most recent version of Hypothesis. Documented APIs will not break except between major version bumps. All APIs mentioned in the Hypothesis documentation are public unless explicitly noted as provisional, in which case they may be changed in minor releases. Undocumented attributes, modules, and behaviour may include breaking changes in patch releases. .. _deprecation-policy: Deprecations ------------ Deprecated features will emit |HypothesisDeprecationWarning| for at least six months, and then be removed in the following major release. Note however that not all warnings are subject to this grace period; sometimes we strengthen validation by adding a warning, and these may become errors immediately at a major release. We use custom exception and warning types, so you can see exactly where an error came from, or turn only our warnings into errors. Python versions --------------- Hypothesis is supported and tested on CPython and PyPy 3.10+, i.e. all Python versions `that are still supported `_. 32-bit builds of CPython also work, though we only test them on Windows. Hypothesis does not officially support anything except the latest patch release of each supported Python version. We will fix bugs in earlier patch releases if reported, but they're not tested in CI and no guarantees are made. Operating systems ----------------- In theory, Hypothesis should work anywhere that Python does. In practice, it is known to work and regularly tested on macOS, Windows, Linux, and `Emscripten `_. If you experience issues running Hypothesis on other operating systems, we are happy to accept bug reports which either clearly point to the problem or contain reproducing instructions for a Hypothesis maintainer who does not have the ability to run that OS. It's hard to fix something we can't reproduce! .. _framework-compatibility: Testing frameworks ------------------ In general, Hypothesis goes to quite a lot of effort to return a function from |@given| that behaves as closely to a normal test function as possible. This means that most things should work sensibly with most testing frameworks. Maintainers of testing frameworks may be interested in our support for :ref:`custom function execution `, which may make some Hypothesis interactions possible to support. pytest ~~~~~~ The main interaction to be aware of between Hypothesis and :pypi:`pytest` is fixtures. pytest fixtures are automatically passed to |@given| tests, as usual. Note that |@given| supplies parameters from the right, so tests which use a fixture should either be written with the fixture placed first, or with keyword arguments: .. code-block:: python @given(st.integers()) def test_use_fixture(myfixture, n): pass @given(n=st.integers()) def test_use_fixture(n, myfixture): pass However, function-scoped fixtures run only once for the entire test, not per-input. This can be surprising for fixtures which are expected to set up per-input state. To proactively warn about this, we raise |HealthCheck.function_scoped_fixture| (unless suppressed with |settings.suppress_health_check|). Combining |@given| and |pytest.mark.parametrize| is fully supported, again keeping in mind that |@given| supplies parameters from the right: .. code-block:: python @pytest.mark.parametrize("s", ["a", "b", "c"]) @given(st.integers()) def test_use_parametrize(s, n): assert isinstance(s, str) assert isinstance(n, int) unittest ~~~~~~~~ :pypi:`unittest` works out of the box with Hypothesis. The :func:`python:unittest.mock.patch` decorator works with |@given|, but we recommend using it as a context manager within the test instead, to ensure that the mock is per-input, and to avoid poor interactions with Pytest fixtures. :meth:`unittest.TestCase.subTest` is a no-op under Hypothesis, because it interacts poorly with Hypothesis generating hundreds of inputs at a time. Django ~~~~~~ Integration with Django's testing requires use of the :ref:`hypothesis-django` extra. The issue is that Django tests reset the database once per test, rather than once per input. :pypi:`pytest-django` doesn't yet implement Hypothesis compatibility. You can follow issue `pytest-django#912 `_ for updates. coverage.py ~~~~~~~~~~~ :pypi:`coverage` works out of the box with Hypothesis. Our own test suite has 100% branch coverage. HypoFuzz ~~~~~~~~ `HypoFuzz `_ is built on top of Hypothesis and has native support for it. See also the ``hypofuzz`` |alternative backend|. Optional packages ----------------- The supported versions of optional packages, for strategies in ``hypothesis.extra``, are listed in the documentation for that extra. Our general goal is to support all versions that are supported upstream. .. _thread-safety-policy: Thread-Safety Policy -------------------- As of :version:`6.136.9`, Hypothesis is thread-safe. Each of the following is fully supported, and tested regularly in CI: * Running tests in multiple processes * Running separate tests in multiple threads * Running the same test in multiple threads If you find a bug here, please report it. The main risks internally are global state, shared caches, and cached strategies. Thread usage inside tests ~~~~~~~~~~~~~~~~~~~~~~~~~ .. TODO_DOCS: link to not-yet-merged flaky failure tutorial page Tests that spawn threads internally are supported by Hypothesis. However, these as with any Hypothesis test, these tests must have deterministic test outcomes and data generation. For example, if timing changes in the threads change the sequence of dynamic draws from |st.composite| or |st.data|, Hypothesis may report the test as flaky. The solution here is to refactor data generation so it does not depend on test timings. Cross-thread API calls ~~~~~~~~~~~~~~~~~~~~~~ In theory, Hypothesis supports cross-thread API calls, for instance spawning a thread inside of a test and using that to draw from |st.composite| or |st.data|, or to call |event|, |target|, or |assume|. However, we have not explicitly audited this behavior, and do not regularly test it in our CI. If you find a bug here, please report it. If our investigation determines that we cannot support cross-thread calls for the feature in question, we will update this page accordingly. Type hints ---------- We ship type hints with Hypothesis itself. Though we always try to minimize breakage, we may make breaking changes to these between minor releases and do not commit to maintaining a fully stable interface for type hints. We may also find more precise ways to describe the type of various interfaces, or change their type and runtime behaviour together in a way which is otherwise backwards-compatible. There are known issues with inferring the type of examples generated by |st.deferred|, |st.recursive|, |st.one_of|, |st.dictionaries|, and |st.fixed_dictionaries|. We're following proposed updates to Python's typing standards, but unfortunately the long-standing interfaces of these strategies cannot (yet) be statically typechecked. ================================================ FILE: hypothesis-python/docs/conf.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime import re import sys import types from pathlib import Path from docutils import nodes from sphinx.util.docutils import SphinxRole root = Path(__file__).parent.parent # insert so the local versions are consulted first, before any installed version sys.path.insert(0, str(root / "src")) sys.path.insert(0, str(Path(__file__).parent / "_ext")) needs_sphinx = re.search( r"sphinx==([0-9\.]+)", root.joinpath("../requirements/tools.txt").read_text() ).group(1) default_role = "py:obj" nitpicky = True autodoc_member_order = "bysource" autodoc_typehints = "none" maximum_signature_line_length = 60 # either one line, or one param per line extensions = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx_codeautolink", "sphinx_selective_exclude.eager_only", "sphinx-jsonschema", # loading this extension overrides the default -b linkcheck behavior with # custom url ignore logic. see hypothesis_linkcheck.py for details. "hypothesis_linkcheck", "hypothesis_redirects", ] templates_path = ["_templates"] # config for hypothesis_redirects redirects = { "details": "reference/index.html", "data": "reference/strategies.html", "database": "reference/api.html#database", # "stateful": "reference/api.html#stateful-tests", "reproducing": "reference/api.html", "ghostwriter": "reference/integrations.html#ghostwriter", "django": "reference/strategies.html#django", "numpy": "reference/strategies.html#numpy", "observability": "reference/integrations.html#observability", "settings": "reference/api.html#settings", "endorsements": "usage.html#testimonials", # TODO enable when we actually rename them # "extras": "extensions.html", "supported": "compatibility.html", "changes": "changelog.html", "strategies": "extensions.html", # these pages were removed without replacement "support": "index.html", "manifesto": "index.html", "examples": "index.html", } redirect_html_template_file = "redirect.html.template" source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "Hypothesis" author = "the Hypothesis team" copyright = f"2013-{datetime.date.today().year}, {author}" _d = {} _version_file = root.joinpath("src", "hypothesis", "version.py") exec(_version_file.read_text(encoding="utf-8"), _d) version = _d["__version__"] release = _d["__version__"] # custom role for version syntaxes. # :v:`6.131.0` = [v6.131.0](changelog.html#v6.13.0) # :version:`6.131.0` = [version 6.131.0](changelog.html#v6.13.0) class VersionRole(SphinxRole): def __init__(self, prefix): self.prefix = prefix def run(self): node = nodes.reference( "", f"{self.prefix}{self.text}", refuri=f"changelog.html#v{self.text.replace('.', '-')}", ) return [node], [] def setup(app): if root.joinpath("RELEASE.rst").is_file(): app.tags.add("has_release_file") # Workaround for partial-initialization problem when autodoc imports libcst import libcst import hypothesis.extra.codemods assert libcst assert hypothesis.extra.codemods # patch in mock array_api namespace so we can autodoc it from hypothesis.extra.array_api import ( RELEASED_VERSIONS, make_strategies_namespace, mock_xp, ) mod = types.ModuleType("xps") mod.__dict__.update( make_strategies_namespace(mock_xp, api_version=RELEASED_VERSIONS[-1]).__dict__ ) assert "xps" not in sys.modules sys.modules["xps"] = mod def process_signature(app, what, name, obj, options, signature, return_annotation): # manually override an ugly signature from .. autofunction. Alternative we # could manually document with `.. function:: run_conformance_test(...)`, # but that's less likely to stay up to date. if ( name == "hypothesis.internal.conjecture.provider_conformance.run_conformance_test" ): # so we know if this ever becomes obsolete assert "_realize_objects" in signature signature = re.sub( r"_realize_objects=.*", "_realize_objects=st.from_type(object) | st.from_type(type).flatmap(st.from_type))", signature, ) return signature, return_annotation app.connect("autodoc-process-signature", process_signature) app.add_role("v", VersionRole(prefix="v")) app.add_role("version", VersionRole(prefix="version ")) language = "en" exclude_patterns = ["_build", "prolog.rst"] pygments_style = "sphinx" todo_include_todos = False # To run linkcheck (last argument is the output dir) # sphinx-build --builder linkcheck hypothesis-python/docs linkcheck linkcheck_ignore = [ # we'll assume that python isn't going to break peps, and github isn't going # to break issues/pulls. (and if they did, we'd hopefully notice quickly). r"https://peps.python.org/pep-.*", r"https://github.com/HypothesisWorks/hypothesis/issues/\d+", r"https://github.com/HypothesisWorks/hypothesis/pull/\d+", ] intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "numpy": ("https://numpy.org/doc/stable/", None), "pandas": ("https://pandas.pydata.org/docs/", None), "pytest": ("https://docs.pytest.org/en/stable/", None), "django": ( "http://docs.djangoproject.com/en/stable/", "http://docs.djangoproject.com/en/stable/_objects/", ), "dateutil": ("https://dateutil.readthedocs.io/en/stable/", None), "redis": ("https://redis.readthedocs.io/en/stable/", None), "attrs": ("https://www.attrs.org/en/stable/", None), "sphinx": ("https://www.sphinx-doc.org/en/master/", None), "IPython": ("https://ipython.readthedocs.io/en/stable/", None), "lark": ("https://lark-parser.readthedocs.io/en/stable/", None), "xarray": ("https://docs.xarray.dev/en/stable/", None), } autodoc_mock_imports = ["numpy", "pandas", "redis", "django", "pytz"] rst_prolog = (Path(__file__).parent / "prolog.rst").read_text() codeautolink_autodoc_inject = False codeautolink_global_preface = """ from hypothesis import * import hypothesis.strategies as st from hypothesis.strategies import * """ # This config value must be a dictionary of external sites, mapping unique # short alias names to a base URL and a prefix. # See http://sphinx-doc.org/ext/extlinks.html _repo = "https://github.com/HypothesisWorks/hypothesis/" extlinks = { "commit": (_repo + "commit/%s", "commit %s"), "gh-file": (_repo + "blob/master/%s", "%s"), "gh-link": (_repo + "%s", "%s"), "issue": (_repo + "issues/%s", "issue #%s"), "pull": (_repo + "pull/%s", "pull request #%s"), "pypi": ("https://pypi.org/project/%s/", "%s"), "bpo": ("https://bugs.python.org/issue%s", "bpo-%s"), "xp-ref": ("https://data-apis.org/array-api/latest/API_specification/%s", "%s"), "wikipedia": ("https://en.wikipedia.org/wiki/%s", "%s"), } # -- Options for HTML output ---------------------------------------------- html_theme = "furo" # remove "Hypothesis documentation" from just below logo on the sidebar html_theme_options = {"sidebar_hide_name": True} html_static_path = ["_static"] html_css_files = [ "better-signatures.css", "wrap-in-tables.css", "no-scroll.css", "dark-fix.css", ] htmlhelp_basename = "Hypothesisdoc" html_favicon = "../../brand/favicon.ico" html_logo = "../../brand/dragonfly-rainbow-150w.svg" # -- Options for LaTeX output --------------------------------------------- latex_elements = {} latex_documents = [ (master_doc, "Hypothesis.tex", "Hypothesis Documentation", author, "manual") ] man_pages = [(master_doc, "hypothesis", "Hypothesis Documentation", [author], 1)] texinfo_documents = [ ( master_doc, "Hypothesis", "Hypothesis Documentation", author, "Hypothesis", "Advanced property-based testing for Python.", "Miscellaneous", ) ] ================================================ FILE: hypothesis-python/docs/development.rst ================================================ ====================== Hypothesis development ====================== Hypothesis development is managed by `David R. MacIver `_ and `Zac Hatfield-Dodds `_, respectively the first author and lead maintainer. *However*, these roles don't include unpaid feature development on Hypothesis. Our roles as leaders of the project are: 1. Helping other people do feature development on Hypothesis 2. Fixing bugs and other code health issues 3. Improving documentation 4. General release management work 5. Planning the general roadmap of the project 6. Doing sponsored development on tasks that are too large or in depth for other people to take on So all new features must either be sponsored or implemented by someone else. That being said, the maintenance team takes an active role in shepherding pull requests and helping people write a new feature (see :gh-file:`CONTRIBUTING.rst` for details and :gh-link:`these examples of how the process goes `). This isn't "patches welcome", it's "we will help you write a patch". .. _release-policy: Release policy ============== Hypothesis releases follow `semantic versioning `_. We maintain backwards-compatibility wherever possible, and use deprecation warnings to mark features that have been superseded by a newer alternative. If you want to detect this, you can upgrade warnings to errors using the :mod:`warnings ` module. We use a rapid release cycle to ensure users get features and bugfixes as soon as possible. Every change to the Hypothesis source is automatically built and published on PyPI as soon as it's merged into master. `We wrote about this process here `_. Project roadmap =============== Hypothesis does not have a long-term release plan. We respond to bug reports as they are made; new features are released as and when someone volunteers to write and maintain them. ================================================ FILE: hypothesis-python/docs/explanation/domain.rst ================================================ Domain and distribution ======================= .. note:: This page is primarily for users who may be familiar with other property-based testing libraries, and who expect control over the distribution of inputs in Hypothesis, via e.g. a ``scale`` parameter for size or a ``frequency`` parameter for relative probabilities. Hypothesis makes a distinction between the *domain* of a strategy, and the *distribution* of a strategy. The *domain* is the set of inputs that should be possible to generate. For instance, in ``lists(integers())``, the domain is lists of integers. For other strategies, particularly those that use |st.composite| or |assume| in their definition, the domain might be more complex. The *distribution* is the probability with which different elements in the domain should be generated. For ``lists(integers())``, should Hypothesis generate many small lists? Large lists? More positive or more negative numbers? etc. Hypothesis takes a philosophical stance that while users may be responsible for selecting the domain, the property-based testing library—not the user—should be responsible for selecting the distribution. As an intentional design choice, Hypothesis therefore lets you control the domain of inputs to your test, but not the distribution. How should I choose a domain for my test? ----------------------------------------- We recommend using the most-general strategy for your test, so that it can in principle generate any edge case for which the test should pass. Limiting the size of generated inputs, and especially limiting the variety of inputs, can all too easily exclude the bug-triggering values from consideration - and be the difference between a test which finds the bug, or fails to do so. Sometimes size limits are necessary for performance reasons, but we recommend limiting your strategies only after you've seen *substantial* slowdowns without limits. Far better to find bugs slowly, than not find them at all - and you can manage performance with the |~settings.phases| or |~settings.max_examples| settings rather than weakening the test. Why not let users control the distribution? ------------------------------------------- There are a few reasons Hypothesis doesn't let users control the distribution. * Humans are pretty bad at choosing bug-finding distributions! Some bugs are "known unknowns": you suspected that a part of the codebase was buggy in a particular way. Others are "unknown unknowns": you didn't know that a bug was possible until stumbling across it. Humans tend to overtune distributions for the former kind of bug, and not enough for the latter. * The ideal strategy distribution depends not only on the codebase, but also on the property being tested. A strategy used in many places may have a good distribution for one property, but not another. * The distribution of inputs is a deeply internal implementation detail. We sometimes change strategy distributions, either explicitly, or implicitly from other work on the Hypothesis engine. Exposing this would lock us into a public API that may make improvements to Hypothesis more difficult. Finally, we think distribution control is better handled with |alternative backends|. If existing backends like ``hypofuzz`` and ``crosshair`` don't suit your needs, you can also write your own. Backends can automatically generalize and adapt to the strategy and property being tested and avoid most of the problems above. We're not saying that control over the distribution isn't useful! We occasionally receive requests to expose the distribution in Hypothesis (`e.g. `__), and users wouldn't be asking for it if it wasn't. However, adding this to the public strategy API would make it easy for users to unknowingly weaken their tests, and would add maintenance overhead to Hypothesis, and so we haven't yet done so. Okay, but what *is* the distribution? ------------------------------------- An exact answer depends on both the strategy or strategies for the tests, and the code being tested - but we can make some general remarks, starting with "it's actually really complicated". Hypothesis' default configuration uses a distribution which is tuned to maximize the chance of finding bugs, in as few executions as possible. We explicitly *don't* aim for a uniform distribution, nor for a 'realistic' distribution of inputs; Hypothesis' goal is to search the domain for a failing input as efficiently as possible. The test case distribution remains an active area of research and development, and we change it whenever we think that would be a net improvement for users. Today, Hypothesis' default distribution is shaped by a wide variety of techniques and heuristics: * some are statically designed into strategies - for example, |st.integers| upweights range endpoints, and samples from a mixed distribution over integer bit-widths. * some are dynamic features of the engine - like replaying prior examples with subsections of the input 'cloned' or otherwise altered, for bugs which trigger only when different fields have the same value (which is otherwise exponentially unlikely). * some vary depending on the code under test - we collect interesting-looking constants from imported source files as seeds for test cases. And as if that wasn't enough, :ref:`alternative backends ` can radically change the distribution again - for example :pypi:`hypofuzz` uses runtime feedback to modify the distribution of inputs as the test runs, to maximize the rate at which we trigger new behaviors in that particular test and code. If Hypothesis' defaults aren't strong enough, we recommend trying Hypofuzz! ================================================ FILE: hypothesis-python/docs/explanation/example-count.rst ================================================ How many times will Hypothesis run my test? =========================================== This is a trickier question than you might expect. The short answer is "exactly |max_examples| times", with the following exceptions: - Less than |max_examples| times, if Hypothesis exhausts the search space early. - More than |max_examples| times, if Hypothesis retries some examples because either: - They failed an |assume| or |.filter| condition, or - They were too large to continue generating. - Either less or more than |max_examples| times, if Hypothesis finds a failing example. Read on for details. Search space exhaustion ----------------------- If Hypothesis detects that there are no more examples left to try, it may stop generating examples before it hits |max_examples|. For example: .. code-block:: python from hypothesis import given, strategies as st calls = 0 @given(st.integers(0, 19)) def test_function(n): global calls calls += 1 test_function() assert calls == 20 This runs ``test_function`` 20 times, not 100, since there are only 20 unique integers to try. The search space tracking in Hypothesis is good, but not perfect. We treat this more as a bonus than something to strive for. .. TODO_DOCS .. .. note:: .. Search space tracking uses the :doc:`choice sequence ` to determine uniqueness of inputs. |assume| and |.filter| ---------------------- If an example fails to satisfy an |assume| or |.filter| condition, Hypothesis will retry generating that example and will not count it towards the |max_examples| limit. For instance: .. code-block:: python from hypothesis import assume, given, strategies as st @given(st.integers()) def test_function(n): assume(n % 2 == 0) will run roughly 200 times, since half of the examples are discarded from the |assume|. Note that while failing an |assume| triggers an immediate retry of the entire example, Hypothesis will try several times in the same example to satisfy a |.filter| condition. This makes expressing the same condition using |.filter| more efficient than |assume|. Also note that even if your code does not explicitly use |assume| or |.filter|, a builtin strategy may still use them and cause retries. We try to directly satisfy conditions where possible instead of relying on rejection sampling, so this should be relatively uncommon. Examples which are too large ---------------------------- For performance reasons, Hypothesis places an internal limit on the size of a single example. If an example exceeds this size limit, we will retry generating it and will not count it towards the |max_examples| limit. (And if we see too many of these large examples, we will raise |HealthCheck.data_too_large|, unless suppressed with |settings.suppress_health_check|). The specific value of this size limit is an undocumented implementation detail. The majority of Hypothesis tests do not come close to hitting it. Failing examples ---------------- If Hypothesis finds a failing example, it stops generation early, and may call the test function additional times during the |Phase.shrink| and |Phase.explain| phases. Sometimes, Hypothesis determines that the initial failing example was already as simple as possible, in which case |Phase.shrink| will not result in additional test executions (but |Phase.explain| might). Regardless of whether Hypothesis runs the test during the shrinking and explain phases, it will always run the minimal failing example one additional time to check for flakiness. For instance, the following trivial test runs with ``n=0`` *twice*, even though it only uses the |Phase.generate| phase: .. code-block:: python from hypothesis import Phase, given, settings, strategies as st @given(st.integers()) @settings(phases=[Phase.generate]) def test_function(n): print(f"called with {n}") assert n != 0 test_function() The first execution finds the initial failure with ``n=0``, and the second execution replays ``n=0`` to ensure the failure is not flaky. ================================================ FILE: hypothesis-python/docs/explanation/index.rst ================================================ Explanations ============ These explanation pages are oriented towards deepening your understanding of Hypothesis, including its design philosophy. .. toctree:: :maxdepth: 1 domain example-count ================================================ FILE: hypothesis-python/docs/extensions.rst ================================================ Third-party extensions ====================== There are a number of open-source community libraries that extend Hypothesis. This page lists some of them; you can find more by searching PyPI `by keyword `_ or `by framework classifier `_. If there's something missing which you think should be here, let us know! .. note:: Being listed on this page does not imply that the Hypothesis maintainers endorse a package. External strategies ------------------- Some packages provide strategies directly: * :pypi:`hypothesis-fspaths` - strategy to generate filesystem paths. * :pypi:`hypothesis-geojson` - strategy to generate `GeoJson `_. * :pypi:`hypothesis-geometry` - strategies to generate geometric objects. * :pypi:`hs-dbus-signature` - strategy to generate arbitrary `D-Bus signatures `_. * :pypi:`hypothesis-sqlalchemy` - strategies to generate :pypi:`SQLAlchemy` objects. * :pypi:`hypothesis-ros` - strategies to generate messages and parameters for the `Robot Operating System `_. * :pypi:`hypothesis-csv` - strategy to generate CSV files. * :pypi:`hypothesis-networkx` - strategy to generate :pypi:`networkx` graphs. * :pypi:`hypothesis-bio` - strategies for bioinformatics data, such as DNA, codons, FASTA, and FASTQ formats. * :pypi:`hypothesis-rdkit` - strategies to generate RDKit molecules and representations such as SMILES and mol blocks * :pypi:`hypothesmith` - strategy to generate syntatically-valid Python code. * :pypi:`hypothesis-torch` - strategy to generate various `Pytorch `_ structures (including tensors and modules). Others provide a function to infer a strategy from some other schema: * :pypi:`hypothesis-jsonschema` - infer strategies from `JSON schemas `_. * :pypi:`lollipop-hypothesis` - infer strategies from :pypi:`lollipop` schemas. * :pypi:`hypothesis-drf` - infer strategies from a :pypi:`djangorestframework` serialiser. * :pypi:`hypothesis-graphql` - infer strategies from `GraphQL schemas `_. * :pypi:`hypothesis-mongoengine` - infer strategies from a :pypi:`mongoengine` model. * :pypi:`hypothesis-pb` - infer strategies from `Protocol Buffer `_ schemas. Or some other custom integration, such as a :ref:`"hypothesis" entry point `: * :pypi:`deal` is a design-by-contract library with built-in Hypothesis support. * :pypi:`icontract-hypothesis` infers strategies from :pypi:`icontract` code contracts. * :pypi:`pandera` schemas all have a ``.strategy()`` method, which returns a strategy for matching :class:`~pandas:pandas.DataFrame`\ s. * :pypi:`Pydantic ` automatically registers constrained types - so :func:`~hypothesis.strategies.builds` and :func:`~hypothesis.strategies.from_type` "just work" regardless of the underlying implementation. Other cool things ----------------- `Tyche `__ (`source `__) is a VSCode extension which provides live insights into your property-based tests, including the distribution of generated inputs and the resulting code coverage. You can `read the research paper here `__. :pypi:`schemathesis` is a tool for testing web applications built with `Open API / Swagger specifications `_. It reads the schema and generates test cases which will ensure that the application is compliant with its schema. The application under test could be written in any language, the only thing you need is a valid API schema in a supported format. Includes CLI and convenient :pypi:`pytest` integration. Powered by Hypothesis and :pypi:`hypothesis-jsonschema`, inspired by the earlier :pypi:`swagger-conformance` library. `Trio `_ is an async framework with "an obsessive focus on usability and correctness", so naturally it works with Hypothesis! :pypi:`pytest-trio` includes :ref:`a custom hook ` that allows ``@given(...)`` to work with Trio-style async test functions, and :pypi:`hypothesis-trio` includes stateful testing extensions to support concurrent programs. :pypi:`pymtl3` is "an open-source Python-based hardware generation, simulation, and verification framework with multi-level hardware modeling support", which ships with Hypothesis integrations to check that all of those levels are equivalent, from function-level to register-transfer level and even to hardware. :pypi:`battle-tested` is a fuzzing tool that will show you how your code can fail - by trying all kinds of inputs and reporting whatever happens. :pypi:`pytest-subtesthack` functions as a workaround for :issue:`377`. :pypi:`returns` uses Hypothesis to verify that Higher Kinded Types correctly implement functor, applicative, monad, and other laws; allowing a declarative approach to be combined with traditional pythonic code. :pypi:`icontract-hypothesis` includes a :ref:`ghostwriter ` for test files and IDE integrations such as `icontract-hypothesis-vim `_, `icontract-hypothesis-pycharm `_, and `icontract-hypothesis-vscode `_ - you can run a quick 'smoke test' with only a few keystrokes for any type-annotated function, even if it doesn't have any contracts! Writing an extension -------------------- .. note:: See :gh-file:`CONTRIBUTING.rst` for more information. New strategies can be added to Hypothesis, or published as an external package on PyPI - either is fine for most strategies. If in doubt, ask! It's generally much easier to get things working outside, because there's more freedom to experiment and fewer requirements in stability and API style. We're happy to review and help with external packages as well as pull requests! If you're thinking about writing an extension, please name it ``hypothesis-{something}`` - a standard prefix makes the community more visible and searching for extensions easier. And make sure you use the ``Framework :: Hypothesis`` trove classifier! On the other hand, being inside gets you access to some deeper implementation features (if you need them) and better long-term guarantees about maintenance. We particularly encourage pull requests for new composable primitives that make implementing other strategies easier, or for widely used types in the standard library. Strategies for other things are also welcome; anything with external dependencies just goes in ``hypothesis.extra``. Tools such as assertion helpers may also need to check whether the current test is using Hypothesis. For that, see |currently_in_test_context|. .. _entry-points: Hypothesis integration via entry points --------------------------------------- If you would like to ship Hypothesis strategies for a custom type - either as part of the upstream library, or as a third-party extension, there's a catch: |st.from_type| only works after the corresponding call to |st.register_type_strategy|, and you'll have the same problem with |register_random|. This means that either - you have to try importing Hypothesis to register the strategy when *your* library is imported, though that's only useful at test time, or - the user has to call a 'register the strategies' helper that you provide before running their tests `Entry points `__ are Python's standard way of automating the latter: when you register a ``"hypothesis"`` entry point in your ``pyproject.toml``, we'll import and run it automatically when *hypothesis* is imported. Nothing happens unless Hypothesis is already in use, and it's totally seamless for downstream users! Let's look at an example. You start by adding a function somewhere in your package that does all the Hypothesis-related setup work: .. code-block:: python # mymodule.py class MyCustomType: def __init__(self, x: int): assert x >= 0, f"got {x}, but only positive numbers are allowed" self.x = x def _hypothesis_setup_hook(): import hypothesis.strategies as st st.register_type_strategy(MyCustomType, st.integers(min_value=0)) and then declare this as your ``"hypothesis"`` entry point: .. code-block:: toml # pyproject.toml # You can list a module to import by dotted name [project.entry-points.hypothesis] _ = "mymodule.a_submodule" # Or name a specific function, and Hypothesis will call it for you [project.entry-points.hypothesis] _ = "mymodule:_hypothesis_setup_hook" And that's all it takes! .. envvar:: HYPOTHESIS_NO_PLUGINS If set, disables automatic loading of all hypothesis plugins. This is probably only useful for our own self-tests, but documented in case it might help narrow down any particularly weird bugs in complex environments. Interaction with :pypi:`pytest-cov` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Because pytest does not load plugins from entrypoints in any particular order, using the Hypothesis entrypoint may import your module before :pypi:`pytest-cov` starts. `This is a known issue `__, but there are workarounds. You can use :command:`coverage run pytest ...` instead of :command:`pytest --cov ...`, opting out of the pytest plugin entirely. Alternatively, you can ensure that Hypothesis is loaded after coverage measurement is started by disabling the entrypoint, and loading our pytest plugin from your ``conftest.py`` instead:: echo "pytest_plugins = ['hypothesis.extra.pytestplugin']\n" > tests/conftest.py pytest -p "no:hypothesispytest" ... Another alternative, which we in fact use in our CI self-tests because it works well also with parallel tests, is to automatically start coverage early for all new processes if an environment variable is set. This automatic starting is set up by the PyPi package :pypi:`coverage_enable_subprocess`. This means all configuration must be done in ``.coveragerc``, and not on the command line:: [run] parallel = True source = ... Then, set the relevant environment variable and run normally:: python -m pip install coverage_enable_subprocess export COVERAGE_PROCESS_START=$PATH/.coveragerc pytest [-n auto] ... coverage combine coverage report .. _alternative-backends: Alternative backends for Hypothesis ----------------------------------- .. seealso:: See also the :ref:`Alternative backends interface ` for details on implementing your own alternative backend. Hypothesis supports alternative backends, which tells Hypothesis how to generate primitive types. This enables powerful generation techniques which are compatible with all parts of Hypothesis, including the database and shrinking. Hypothesis includes the following backends: hypothesis The default backend. hypothesis-urandom The same as the default backend, but uses ``/dev/urandom`` as its source of randomness, instead of the standard PRNG in |random.Random|. The only reason to use this backend over the default ``backend="hypothesis"`` is if you are also using `Antithesis `_, in which case this allows Antithesis mutation of ``/dev/urandom`` to control the values generated by Hypothesis. ``/dev/urandom`` is not available on Windows, so we emit a warning and fall back to the hypothesis backend there. hypofuzz Generates inputs using coverage-guided fuzzing. See `HypoFuzz `_ for details. Requires ``pip install hypofuzz``. crosshair Generates inputs using SMT solvers like z3, which is particularly effective at satisfying difficult checks in your code, like ``if`` or ``==`` statements. Requires ``pip install hypothesis[crosshair]``. You can change the backend for a test with the |settings.backend| setting. For instance, after ``pip install hypothesis[crosshair]``, you can use :pypi:`crosshair ` to generate inputs with SMT via the :pypi:`hypothesis-crosshair` backend: .. code-block:: python from hypothesis import given, settings, strategies as st @settings(backend="crosshair") # pip install hypothesis[crosshair] @given(st.integers()) def test_needs_solver(x): assert x != 123456789 Failures found by alternative backends are saved to the database and shrink just like normally generated inputs, and in general interact with every feature of Hypothesis as you would expect. ================================================ FILE: hypothesis-python/docs/extras.rst ================================================ ====================== First-party extensions ====================== Hypothesis has minimal dependencies, to maximise compatibility and make installing Hypothesis as easy as possible. Our integrations with specific packages are therefore provided by ``extra`` modules that need their individual dependencies installed in order to work. You can install these dependencies using the setuptools extra feature as e.g. ``pip install hypothesis[django]``. This will check installation of compatible versions. You can also just install hypothesis into a project using them, ignore the version constraints, and hope for the best. In general "Which version is Hypothesis compatible with?" is a hard question to answer and even harder to regularly test. Hypothesis is always tested against the latest compatible version and each package will note the expected compatibility range. If you run into a bug with any of these please specify the dependency version. The following lists the available extras in Hypothesis and their documentation. * :ref:`hypothesis[cli] ` * :ref:`hypothesis[codemods] ` * :ref:`hypothesis[django] ` * :ref:`hypothesis[numpy] ` * :ref:`hypothesis[lark] ` * :ref:`hypothesis[pytz] ` * :ref:`hypothesis[dateutil] ` * :ref:`hypothesis[dpcontracts] ` ================================================ FILE: hypothesis-python/docs/how-to/custom-database.rst ================================================ Write a custom Hypothesis database ================================== To define your own |ExampleDatabase| class, implement the |ExampleDatabase.save|, |ExampleDatabase.fetch|, and |ExampleDatabase.delete| methods. For example, here's a simple database class that uses :mod:`sqlite ` as the backing data store: .. code-block:: python import sqlite3 from collections.abc import Iterable from hypothesis.database import ExampleDatabase class SQLiteExampleDatabase(ExampleDatabase): def __init__(self, db_path: str): self.conn = sqlite3.connect(db_path) self.conn.execute( """ CREATE TABLE examples ( key BLOB, value BLOB, UNIQUE (key, value) ) """ ) def save(self, key: bytes, value: bytes) -> None: self.conn.execute( "INSERT OR IGNORE INTO examples VALUES (?, ?)", (key, value), ) def fetch(self, key: bytes) -> Iterable[bytes]: cursor = self.conn.execute("SELECT value FROM examples WHERE key = ?", (key,)) yield from [value[0] for value in cursor.fetchall()] def delete(self, key: bytes, value: bytes) -> None: self.conn.execute( "DELETE FROM examples WHERE key = ? AND value = ?", (key, value), ) Database classes are not required to implement |ExampleDatabase.move|. The default implementation of a move is a |ExampleDatabase.delete| of the value in the old key, followed by a |ExampleDatabase.save| of the value in the new key. You can override |ExampleDatabase.move| to override this behavior, if for instance the backing store offers a more efficient move implementation. Change listening ---------------- To support change listening in a database class, you should call |ExampleDatabase._broadcast_change| whenever a value is saved, deleted, or moved in the backing database store. How you track this depends on the details of the database class. For instance, in |DirectoryBasedExampleDatabase|, Hypothesis installs a filesystem monitor via :pypi:`watchdog` in order to broadcast change events. Two useful related methods are |ExampleDatabase._start_listening| and |ExampleDatabase._stop_listening|, which a database class can override to know when to start or stop expensive listening operations. See documentation for details. ================================================ FILE: hypothesis-python/docs/how-to/detect-hypothesis-tests.rst ================================================ Detect Hypothesis tests ----------------------- How to dynamically determine whether a test function has been defined with Hypothesis. Via ``is_hypothesis_test`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ The most straightforward way is to use |is_hypothesis_test|: .. code-block:: python from hypothesis import is_hypothesis_test @given(st.integers()) def f(n): ... assert is_hypothesis_test(f) This works for stateful tests as well: .. code-block:: python from hypothesis import is_hypothesis_test from hypothesis.stateful import RuleBasedStateMachine class MyStateMachine(RuleBasedStateMachine): ... assert is_hypothesis_test(MyStateMachine.TestCase().runTest) Via pytest ~~~~~~~~~~ If you're working with :pypi:`pytest`, the :ref:`Hypothesis pytest plugin ` automatically adds the ``@pytest.mark.hypothesis`` mark to all Hypothesis tests. You can use ``node.get_closest_marker("hypothesis")`` or similar methods to detect the existence of this mark. ================================================ FILE: hypothesis-python/docs/how-to/external-fuzzers.rst ================================================ Use Hypothesis with an external fuzzer ====================================== Sometimes you might want to point a traditional fuzzer like `python-afl `__ or Google's :pypi:`atheris` at your code, to get coverage-guided exploration of native C extensions. The associated tooling is often much less mature than property-based testing libraries though, so you might want to use Hypothesis strategies to describe your input data, and our world-class shrinking and :ref:`observability ` tools to wrangle the results. That's exactly what this how-to guide is about! .. note:: If you already have Hypothesis tests and want to fuzz them, or are targeting pure Python code, we strongly recommend the purpose-built `HypoFuzz `_. This page is about writing traditional 'fuzz harnesses' with an external fuzzer, using parts of Hypothesis. In order to support this workflow, Hypothesis exposes the |fuzz_one_input| method. |fuzz_one_input| takes a bytestring, parses it into a test case, and executes the corresponding test once. This means you can treat each of your Hypothesis tests as a traditional fuzz target, by pointing the fuzzer at |fuzz_one_input|. For example: .. code-block:: python from hypothesis import given, strategies as st @given(st.integers()) def test_ints(n): pass # this parses the bytestring into a test case using st.integers(), # and then executes `test_ints` once. test_ints.hypothesis.fuzz_one_input(b"\x00" * 50) Note that |fuzz_one_input| bypasses the standard test lifecycle. In a standard test run, Hypothesis is responsible for managing the lifecycle of a test, for example by moving between each |Phase|. In contrast, |fuzz_one_input| executes one test case, independent of this lifecycle. See the documentation of |fuzz_one_input| for details of how it interacts with other features of Hypothesis, such as |@settings|. Worked example: using Atheris ----------------------------- Here is an example that uses |fuzz_one_input| with the `Atheris `__ coverage-guided fuzzer (which is built on top of `libFuzzer `_): .. code-block:: python import json import sys import atheris from hypothesis import given, strategies as st @given( st.recursive( st.none() | st.booleans() | st.integers() | st.floats() | st.text(), lambda j: st.lists(j) | st.dictionaries(st.text(), j), ) ) def test_json_dumps_valid_json(value): json.dumps(value) atheris.Setup(sys.argv, test_json_dumps_valid_json.hypothesis.fuzz_one_input) atheris.Fuzz() Generating valid JSON objects based only on Atheris' ``FuzzDataProvider`` interface would be considerably more difficult. You may also want to use ``atheris.instrument_all`` or ``atheris.instrument_imports`` in order to add coverage instrumentation to Atheris. See the `Atheris `__ documentation for full details. ================================================ FILE: hypothesis-python/docs/how-to/index.rst ================================================ How-to guides ============= These how-to pages are practical guides for applying Hypothesis in specific scenarios. Each page addresses a particular question about using Hypothesis. .. toctree:: :maxdepth: 1 suppress-healthchecks type-strategies custom-database detect-hypothesis-tests external-fuzzers ================================================ FILE: hypothesis-python/docs/how-to/suppress-healthchecks.rst ================================================ Suppress a health check everywhere ================================== Hypothesis sometimes raises a |HealthCheck| to indicate that your test may be less effective than you expect, slower than you expect, unlikely to generate effective examples, or otherwise has silently degraded performance. While |HealthCheck| can be useful to proactively identify issues, you may not care about certain classes of them. If you want to disable a |HealthCheck| everywhere, you can register and load a settings profile with |settings.register_profile| and |settings.load_profile|. Place the following code in any file which is loaded before running your tests (or in ``conftest.py``, if using pytest): .. code-block:: python from hypothesis import HealthCheck, settings settings.register_profile( "my_profile", suppress_health_check=[HealthCheck.filter_too_much] ) settings.load_profile("my_profile") This profile in particular suppresses the |HealthCheck.filter_too_much| health check for all tests. The exception is if a test has a |@settings| which explicitly sets a different value for ``suppress_health_check``, in which case the profile value will be overridden by the local settings value. I want to suppress all health checks! ------------------------------------- .. warning:: We strongly recommend that you suppress health checks as you encounter them, rather than using a blanket suppression. Several health checks check for subtle interactions that may save you hours of debugging, such as |HealthCheck.function_scoped_fixture| and |HealthCheck.differing_executors|. If you really want to suppress *all* health checks, for instance to speed up interactive prototyping, you can: .. code-block:: python from hypothesis import HealthCheck, settings settings.register_profile("my_profile", suppress_health_check=list(HealthCheck)) settings.load_profile("my_profile") ================================================ FILE: hypothesis-python/docs/how-to/type-strategies.rst ================================================ Write type hints for strategies =============================== Hypothesis provides type hints for all strategies and functions which return a strategy: .. code-block:: python from hypothesis import strategies as st reveal_type(st.integers()) # SearchStrategy[int] reveal_type(st.lists(st.integers())) # SearchStrategy[list[int]] |SearchStrategy| is the type of a strategy. It is parametrized by the type of the example it generates. You can use it to write type hints for your functions which return a strategy: .. code-block:: python from hypothesis import strategies as st from hypothesis.strategies import SearchStrategy # returns a strategy for "normal" numbers def numbers() -> SearchStrategy[int | float]: return st.integers() | st.floats(allow_nan=False, allow_infinity=False) It's worth pointing out the distinction between a strategy, and a function that returns a strategy. |st.integers| is a function which returns a strategy, and that strategy has type ``SearchStrategy[int]``. The function ``st.integers`` therefore has type ``Callable[..., SearchStrategy[int]]``, while the value ``s = st.integers()`` has type ``SearchStrategy[int]``. Type hints for |st.composite| ----------------------------- When writing type hints for strategies defined with |st.composite|, use the type of the returned value (not ``SearchStrategy``): .. code-block:: python @st.composite def ordered_pairs(draw) -> tuple[int, int]: n1 = draw(st.integers()) n2 = draw(st.integers(min_value=n1)) return (n1, n2) Type variance of |SearchStrategy| --------------------------------- |SearchStrategy| is covariant, meaning that if ``B < A`` then ``SearchStrategy[B] < SearchStrategy[A]``. In other words, the strategy ``st.from_type(Dog)`` is a subtype of the strategy ``st.from_type(Animal)``. ================================================ FILE: hypothesis-python/docs/index.rst ================================================ .. raw:: html Welcome to Hypothesis! ====================== Hypothesis is the property-based testing library for Python. With Hypothesis, you write tests which should pass for all inputs in whatever range you describe, and let Hypothesis randomly choose which of those inputs to check - including edge cases you might not have thought about. For example: .. code-block:: python from hypothesis import given, strategies as st @given(st.lists(st.integers() | st.floats())) def test_sort_correctness_using_properties(lst): result = my_sort(lst) assert set(lst) == set(result) assert all(a <= b for a, b in zip(result, result[1:])) You should start with the :doc:`tutorial `, or alternatively the more condensed :doc:`quickstart `. .. rst-class:: row .. rst-class:: column column2 top-left :doc:`Tutorial ` --------------------------------- An introduction to Hypothesis. New users should start here, or with the more condensed :doc:`quickstart `. .. rst-class:: column column2 top-right :doc:`How-to guides ` ----------------------------------- Practical guides for applying Hypothesis in specific scenarios. .. rst-class:: column column2 bottom-left :doc:`Explanations ` --------------------------------------- Commentary oriented towards deepening your understanding of Hypothesis. .. rst-class:: column column2 bottom-right :doc:`API Reference ` -------------------------------------- Technical API reference. .. rst-class:: row .. toctree:: :maxdepth: 1 :hidden: quickstart tutorial/index how-to/index explanation/index reference/index stateful Extras changelog .. toctree:: :maxdepth: 1 :hidden: :caption: About Hypothesis development compatibility usage extensions packaging community ================================================ FILE: hypothesis-python/docs/packaging.rst ================================================ ==================== Packaging guidelines ==================== Downstream packagers often want to package Hypothesis. Here are some guidelines. The primary guideline is this: If you are not prepared to keep up with the Hypothesis release schedule, don't. You will be doing your users a disservice. Hypothesis has a very frequent release schedule. We often release new versions multiple times a week. If you *are* prepared to keep up with this schedule, you might find the rest of this document useful. ---------------- Release tarballs ---------------- These are available from :gh-link:`the GitHub releases page `. The tarballs on PyPI are intended for installation from a Python tool such as :pypi:`pip` and should not be considered complete releases. Requests to include additional files in them will not be granted. Their absence is not a bug. ------------ Dependencies ------------ ~~~~~~~~~~~~~~~ Python versions ~~~~~~~~~~~~~~~ Hypothesis is designed to work with a range of Python versions - we support `all versions of CPython with upstream support `_. We also support the latest versions of PyPy for Python 3. ~~~~~~~~~~~~~~~~~~~~~~ Other Python libraries ~~~~~~~~~~~~~~~~~~~~~~ Hypothesis has *mandatory* dependencies on the following libraries: * :pypi:`sortedcontainers` Hypothesis has *optional* dependencies on the following libraries: .. literalinclude:: ../pyproject.toml :prepend: [project.optional-dependencies] :start-after: [project.optional-dependencies] :end-before: # Avoid changing this by hand The way this works when installing Hypothesis normally is that these features become available if the relevant library is installed. Specifically for :pypi:`pytest`, our plugin supports versions of pytest which have been out of upstream support for some time. Hypothesis tests can still be executed by even older versions of pytest - you just won't have the plugin to provide automatic marks, helpful usage warnings, and per-test statistics. ------------------ Testing Hypothesis ------------------ If you want to test Hypothesis as part of your packaging you will probably not want to use the mechanisms Hypothesis itself uses for running its tests, because it has a lot of logic for installing and testing against different versions of Python. The tests must be run with fairly recent tooling; check the :gh-link:`tree/master/requirements/` directory for details. The organisation of the tests is described in the :gh-file:`hypothesis-python/tests/README.rst`. -------- Examples -------- * `arch linux `_ * `fedora `_ * `gentoo `_ ================================================ FILE: hypothesis-python/docs/prolog.rst ================================================ .. |given| replace:: :func:`~hypothesis.given` .. |@given| replace:: :func:`@given ` .. |@example| replace:: :func:`@example ` .. |@example.xfail| replace:: :func:`@example(...).xfail() ` .. |@example.via| replace:: :func:`@example(...).via() ` .. |example.xfail| replace:: :func:`example.xfail ` .. |example.via| replace:: :func:`example.via ` .. |@settings| replace:: :func:`@settings ` .. |settings| replace:: :func:`settings ` .. |@composite| replace:: :func:`@composite ` .. |assume| replace:: :func:`~hypothesis.assume` .. |target| replace:: :func:`~hypothesis.target` .. |event| replace:: :func:`~hypothesis.event` .. |note| replace:: :func:`~hypothesis.note` .. |infer| replace:: :data:`~hypothesis.infer` .. |fuzz_one_input| replace:: :func:`~hypothesis.core.HypothesisHandle.fuzz_one_input` .. |max_examples| replace:: :obj:`~hypothesis.settings.max_examples` .. |settings.max_examples| replace:: :obj:`settings.max_examples ` .. |settings.database| replace:: :obj:`settings.database ` .. |settings.deadline| replace:: :obj:`settings.deadline ` .. |settings.derandomize| replace:: :obj:`settings.derandomize ` .. |settings.phases| replace:: :obj:`settings.phases ` .. |settings.print_blob| replace:: :obj:`settings.print_blob ` .. |settings.report_multiple_bugs| replace:: :obj:`settings.report_multiple_bugs ` .. |settings.verbosity| replace:: :obj:`settings.verbosity ` .. |settings.suppress_health_check| replace:: :obj:`settings.suppress_health_check ` .. |settings.stateful_step_count| replace:: :obj:`settings.stateful_step_count ` .. |settings.backend| replace:: :obj:`settings.backend ` .. |~settings.max_examples| replace:: :obj:`~hypothesis.settings.max_examples` .. |~settings.database| replace:: :obj:`~hypothesis.settings.database` .. |~settings.deadline| replace:: :obj:`~hypothesis.settings.deadline` .. |~settings.derandomize| replace:: :obj:`~hypothesis.settings.derandomize` .. |~settings.phases| replace:: :obj:`~hypothesis.settings.phases` .. |~settings.print_blob| replace:: :obj:`~hypothesis.settings.print_blob` .. |~settings.report_multiple_bugs| replace:: :obj:`~hypothesis.settings.report_multiple_bugs` .. |~settings.verbosity| replace:: :obj:`~hypothesis.settings.verbosity` .. |~settings.suppress_health_check| replace:: :obj:`~hypothesis.settings.suppress_health_check` .. |~settings.stateful_step_count| replace:: :obj:`~hypothesis.settings.stateful_step_count` .. |~settings.backend| replace:: :obj:`~hypothesis.settings.backend` .. |settings.register_profile| replace:: :func:`~hypothesis.settings.register_profile` .. |settings.get_profile| replace:: :func:`~hypothesis.settings.get_profile` .. |settings.load_profile| replace:: :func:`~hypothesis.settings.load_profile` .. |settings.get_current_profile_name| replace:: :func:`~hypothesis.settings.get_current_profile_name` .. |HealthCheck.data_too_large| replace:: :obj:`HealthCheck.data_too_large ` .. |HealthCheck.filter_too_much| replace:: :obj:`HealthCheck.filter_too_much ` .. |HealthCheck.too_slow| replace:: :obj:`HealthCheck.too_slow ` .. |HealthCheck.function_scoped_fixture| replace:: :obj:`HealthCheck.function_scoped_fixture ` .. |HealthCheck.differing_executors| replace:: :obj:`HealthCheck.differing_executors ` .. |HealthCheck| replace:: :obj:`~hypothesis.HealthCheck` .. |Phase| replace:: :obj:`Phase ` .. |Phase.explicit| replace:: :obj:`Phase.explicit ` .. |Phase.reuse| replace:: :obj:`Phase.reuse ` .. |Phase.generate| replace:: :obj:`Phase.generate ` .. |Phase.target| replace:: :obj:`Phase.target ` .. |Phase.shrink| replace:: :obj:`Phase.shrink ` .. |Phase.explain| replace:: :obj:`Phase.explain ` .. |Verbosity| replace:: :obj:`~hypothesis.Verbosity` .. |Verbosity.verbose| replace:: :obj:`Verbosity.verbose ` .. |Verbosity.debug| replace:: :obj:`Verbosity.debug ` .. |Verbosity.normal| replace:: :obj:`Verbosity.normal ` .. |Verbosity.quiet| replace:: :obj:`Verbosity.quiet ` .. |HypothesisException| replace:: :obj:`HypothesisException ` .. |HypothesisDeprecationWarning| replace:: :obj:`HypothesisDeprecationWarning ` .. |NonInteractiveExampleWarning| replace:: :obj:`NonInteractiveExampleWarning ` .. |Flaky| replace:: :obj:`Flaky ` .. |FlakyStrategyDefinition| replace:: :obj:`FlakyStrategyDefinition ` .. |FlakyFailure| replace:: :obj:`FlakyFailure ` .. |FlakyBackendFailure| replace:: :obj:`~hypothesis.errors.FlakyBackendFailure` .. |InvalidArgument| replace:: :obj:`InvalidArgument ` .. |DidNotReproduce| replace:: :obj:`DidNotReproduce ` .. |DeadlineExceeded| replace:: :obj:`DeadlineExceeded ` .. |BackendCannotProceed| replace:: :obj:`~hypothesis.errors.BackendCannotProceed` .. |st.lists| replace:: :func:`~hypothesis.strategies.lists` .. |st.integers| replace:: :func:`~hypothesis.strategies.integers` .. |st.floats| replace:: :func:`~hypothesis.strategies.floats` .. |st.booleans| replace:: :func:`~hypothesis.strategies.booleans` .. |st.none| replace:: :func:`~hypothesis.strategies.none` .. |st.composite| replace:: :func:`@composite ` .. |st.data| replace:: :func:`~hypothesis.strategies.data` .. |st.one_of| replace:: :func:`~hypothesis.strategies.one_of` .. |st.text| replace:: :func:`~hypothesis.strategies.text` .. |st.characters| replace:: :func:`~hypothesis.strategies.characters` .. |st.tuples| replace:: :func:`~hypothesis.strategies.tuples` .. |st.sets| replace:: :func:`~hypothesis.strategies.sets` .. |st.dictionaries| replace:: :func:`~hypothesis.strategies.dictionaries` .. |st.fixed_dictionaries| replace:: :func:`~hypothesis.strategies.fixed_dictionaries` .. |st.datetimes| replace:: :func:`~hypothesis.strategies.datetimes` .. |st.builds| replace:: :func:`~hypothesis.strategies.builds` .. |st.recursive| replace:: :func:`~hypothesis.strategies.recursive` .. |st.deferred| replace:: :func:`~hypothesis.strategies.deferred` .. |st.from_type| replace:: :func:`~hypothesis.strategies.from_type` .. |st.sampled_from| replace:: :func:`~hypothesis.strategies.sampled_from` .. |st.shared| replace:: :func:`~hypothesis.strategies.shared` .. |st.uuids| replace:: :func:`~hypothesis.strategies.uuids` .. |st.ip_addresses| replace:: :func:`~hypothesis.strategies.ip_addresses` .. |st.register_type_strategy| replace:: :func:`~hypothesis.strategies.register_type_strategy` .. |st.just| replace:: :func:`~hypothesis.strategies.just` .. |st.domains| replace:: :func:`~hypothesis.provisional.domains` .. |st.urls| replace:: :func:`~hypothesis.provisional.urls` .. |register_random| replace:: :func:`~hypothesis.register_random` .. |st.register_random| replace:: :func:`~hypothesis.register_random` .. |django.from_form| replace:: :func:`~hypothesis.extra.django.from_form` .. |django.from_model| replace:: :func:`~hypothesis.extra.django.from_model` .. |django.from_field| replace:: :func:`~hypothesis.extra.django.from_field` .. |SearchStrategy| replace:: :class:`~hypothesis.strategies.SearchStrategy` .. |filter| replace:: :func:`.filter() ` .. |.filter| replace:: :func:`.filter() ` .. |.filter()| replace:: :func:`.filter() ` .. |flatmap| replace:: :func:`.flatmap() ` .. |.flatmap| replace:: :func:`.flatmap() ` .. |.flatmap()| replace:: :func:`.flatmap() ` .. |map| replace:: :func:`.map() ` .. |.map| replace:: :func:`.map() ` .. |.map()| replace:: :func:`.map() ` .. |.example| replace:: :func:`.example() ` .. |.example()| replace:: :func:`.example() ` .. |PrimitiveProvider| replace:: :class:`~hypothesis.internal.conjecture.providers.PrimitiveProvider` .. |PrimitiveProvider.realize| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.realize` .. |PrimitiveProvider.draw_integer| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_integer` .. |PrimitiveProvider.draw_boolean| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_boolean` .. |PrimitiveProvider.draw_float| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_float` .. |PrimitiveProvider.draw_string| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_string` .. |PrimitiveProvider.draw_bytes| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_bytes` .. |PrimitiveProvider.on_observation| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.on_observation` .. |PrimitiveProvider.observe_test_case| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.observe_test_case` .. |PrimitiveProvider.observe_information_messages| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.observe_information_messages` .. |PrimitiveProvider.per_test_case_context_manager| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.per_test_case_context_manager` .. |PrimitiveProvider.add_observability_callback| replace:: :data:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.add_observability_callback` .. |PrimitiveProvider.span_start| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.span_start` .. |PrimitiveProvider.span_end| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.span_end` .. |PrimitiveProvider.avoid_realization| replace:: :data:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.avoid_realization` .. |AVAILABLE_PROVIDERS| replace:: :data:`~hypothesis.internal.conjecture.providers.AVAILABLE_PROVIDERS` .. |run_conformance_test| replace:: :func:`~hypothesis.internal.conjecture.provider_conformance.run_conformance_test` .. |add_observability_callback| replace:: :data:`~hypothesis.internal.observability.add_observability_callback` .. |remove_observability_callback| replace:: :data:`~hypothesis.internal.observability.remove_observability_callback` .. |with_observability_callback| replace:: :data:`~hypothesis.internal.observability.with_observability_callback` .. |observability_enabled| replace:: :data:`~hypothesis.internal.observability.observability_enabled` .. |TESTCASE_CALLBACKS| replace:: :data:`~hypothesis.internal.observability.TESTCASE_CALLBACKS` .. |OBSERVABILITY_CHOICES| replace:: :data:`~hypothesis.internal.observability.OBSERVABILITY_CHOICES` .. |BUFFER_SIZE| replace:: :data:`~hypothesis.internal.conjecture.engine.BUFFER_SIZE` .. |MAX_SHRINKS| replace:: :data:`~hypothesis.internal.conjecture.engine.MAX_SHRINKS` .. |MAX_SHRINKING_SECONDS| replace:: :data:`~hypothesis.internal.conjecture.engine.MAX_SHRINKING_SECONDS` .. |@rule| replace:: :func:`@rule ` .. |@precondition| replace:: :func:`@precondition ` .. |@initialize| replace:: :func:`@initialize ` .. |@invariant| replace:: :func:`@invariant ` .. |RuleBasedStateMachine| replace:: :class:`~hypothesis.stateful.RuleBasedStateMachine` .. |run_state_machine_as_test| replace:: :func:`~hypothesis.stateful.run_state_machine_as_test` .. |@reproduce_failure| replace:: :func:`@reproduce_failure ` .. |@seed| replace:: :func:`@seed ` .. |ExampleDatabase| replace:: :class:`~hypothesis.database.ExampleDatabase` .. |ExampleDatabase.save| replace:: :func:`~hypothesis.database.ExampleDatabase.save` .. |ExampleDatabase.delete| replace:: :func:`~hypothesis.database.ExampleDatabase.delete` .. |ExampleDatabase.fetch| replace:: :func:`~hypothesis.database.ExampleDatabase.fetch` .. |ExampleDatabase.move| replace:: :func:`~hypothesis.database.ExampleDatabase.move` .. |ExampleDatabase.add_listener| replace:: :func:`~hypothesis.database.ExampleDatabase.add_listener` .. |ExampleDatabase.remove_listener| replace:: :func:`~hypothesis.database.ExampleDatabase.remove_listener` .. |ExampleDatabase.clear_listeners| replace:: :func:`~hypothesis.database.ExampleDatabase.clear_listeners` .. |ExampleDatabase._start_listening| replace:: :func:`~hypothesis.database.ExampleDatabase._start_listening` .. |ExampleDatabase._stop_listening| replace:: :func:`~hypothesis.database.ExampleDatabase._stop_listening` .. |ExampleDatabase._broadcast_change| replace:: :func:`~hypothesis.database.ExampleDatabase._broadcast_change` .. |DirectoryBasedExampleDatabase| replace:: :class:`~hypothesis.database.DirectoryBasedExampleDatabase` .. |InMemoryExampleDatabase| replace:: :class:`~hypothesis.database.InMemoryExampleDatabase` .. |ReadOnlyDatabase| replace:: :class:`~hypothesis.database.ReadOnlyDatabase` .. |MultiplexedDatabase| replace:: :class:`~hypothesis.database.MultiplexedDatabase` .. |GitHubArtifactDatabase| replace:: :class:`~hypothesis.database.GitHubArtifactDatabase` .. |BackgroundWriteDatabase| replace:: :class:`~hypothesis.database.BackgroundWriteDatabase` .. |RedisExampleDatabase| replace:: :class:`~hypothesis.extra.redis.RedisExampleDatabase` .. |is_hypothesis_test| replace:: :func:`~hypothesis.is_hypothesis_test` .. |currently_in_test_context| replace:: :func:`~hypothesis.currently_in_test_context` .. |hypothesis-numpy| replace:: :ref:`NumPy ` .. |hypothesis-pandas| replace:: :ref:`pandas ` .. |hypothesis-django| replace:: :ref:`Django ` .. |str| replace:: :obj:`python:str` .. |int| replace:: :obj:`python:int` .. |bool| replace:: :obj:`python:bool` .. |bytes| replace:: :obj:`python:bytes` .. |float| replace:: :obj:`python:float` .. |assert| replace:: :keyword:`python:assert` .. |memoryview| replace:: :class:`python:memoryview` .. |dataclasses| replace:: :mod:`python:dataclasses` .. |random.random| replace:: :class:`python:random.Random` .. |random.Random| replace:: :class:`python:random.Random` .. |ellipsis| replace:: :obj:`python:Ellipsis` .. |Ellipsis| replace:: :obj:`python:Ellipsis` .. |TypeAlias| replace:: :obj:`python:typing.TypeAlias` .. |TypeAliasType| replace:: :class:`python:typing.TypeAliasType` .. |NewType| replace:: :class:`python:typing.NewType` .. |alternative backend| replace:: :ref:`alternative backend ` .. |alternative backends| replace:: :ref:`alternative backends ` .. |pytest.mark.parametrize| replace:: :ref:`pytest.mark.parametrize ` ================================================ FILE: hypothesis-python/docs/quickstart.rst ================================================ Quickstart ========== This is a lightning introduction to the most important features of Hypothesis; enough to get you started writing tests. The :doc:`tutorial ` introduces these features (and more) in greater detail. Install Hypothesis ------------------ .. code-block:: shell pip install hypothesis Write your first test --------------------- Create a new file called ``example.py``, containing a simple test: .. code-block:: python # contents of example.py from hypothesis import given, strategies as st @given(st.integers()) def test_integers(n): print(f"called with {n}") assert isinstance(n, int) test_integers() |@given| is the standard entrypoint to Hypothesis. It takes a *strategy*, which describes the type of inputs you want the decorated function to accept. When we call ``test_integers``, Hypothesis will generate random integers (because we used the |st.integers| strategy) and pass them as ``n``. Let's see that in action now by running ``python example.py``: .. code-block:: none called with 0 called with -18588 called with -672780074 called with 32616 ... We just called ``test_integers()``, without passing a value for ``n``, because Hypothesis generates random values of ``n`` for us. .. note:: By default, Hypothesis generates 100 random inputs. You can control this with the |max_examples| setting. Running in a test suite ----------------------- A Hypothesis test is still a regular python function, which means pytest or unittest will pick it up and run it in all the normal ways. .. code-block:: python # contents of example.py from hypothesis import given, strategies as st @given(st.integers(0, 200)) def test_integers(n): assert n < 50 This test will clearly fail, which can be confirmed by running ``pytest example.py``: .. code-block:: none $ pytest example.py ... @given(st.integers()) def test_integers(n): > assert n < 50 E assert 50 < 50 E Falsifying example: test_integers( E n=50, E ) Arguments to |@given| --------------------- You can pass multiple arguments to |@given|: .. code-block:: python @given(st.integers(), st.text()) def test_integers(n, s): assert isinstance(n, int) assert isinstance(s, str) Or use keyword arguments: .. code-block:: python @given(n=st.integers(), s=st.text()) def test_integers(n, s): assert isinstance(n, int) assert isinstance(s, str) .. note:: See |@given| for details about how |@given| handles different types of arguments. Filtering inside a test ----------------------- Sometimes, you need to remove invalid cases from your test. The best way to do this is with |.filter|: .. code-block:: python @given(st.integers().filter(lambda n: n % 2 == 0)) def test_integers(n): assert n % 2 == 0 For more complicated conditions, you can use |assume|, which tells Hypothesis to discard any test case with a false-y argument: .. code-block:: python @given(st.integers(), st.integers()) def test_integers(n1, n2): assume(n1 != n2) # n1 and n2 are guaranteed to be different here .. note:: You can learn more about |.filter| and |assume| in the :doc:`/tutorial/adapting-strategies` tutorial page. Dependent generation -------------------- You may want an input to depend on the value of another input. For instance, you might want to generate two integers ``n1`` and ``n2`` where ``n1 <= n2``. You can do this using the |st.composite| strategy. |st.composite| lets you define a new strategy which is itself built by drawing values from other strategies, using the automatically-passed ``draw`` function. .. code-block:: python @st.composite def ordered_pairs(draw): n1 = draw(st.integers()) n2 = draw(st.integers(min_value=n1)) return (n1, n2) @given(ordered_pairs()) def test_pairs_are_ordered(pair): n1, n2 = pair assert n1 <= n2 In more complex cases, you might need to interleave generation and test code. In this case, use |st.data|. .. code-block:: python @given(st.data(), st.text(min_size=1)) def test_string_characters_are_substrings(data, string): assert isinstance(string, str) index = data.draw(st.integers(0, len(string) - 1)) assert string[index] in string Combining Hypothesis with pytest -------------------------------- Hypothesis works with pytest features, like |pytest.mark.parametrize|: .. code-block:: python import pytest from hypothesis import given, strategies as st @pytest.mark.parametrize("operation", [reversed, sorted]) @given(st.lists(st.integers())) def test_list_operation_preserves_length(operation, lst): assert len(lst) == len(list(operation(lst))) Hypothesis also works with pytest fixtures: .. code-block:: python import pytest @pytest.fixture(scope="session") def shared_mapping(): return {n: 0 for n in range(101)} @given(st.integers(0, 100)) def test_shared_mapping_keys(shared_mapping, n): assert n in shared_mapping ================================================ FILE: hypothesis-python/docs/redirect.html.template ================================================ ================================================ FILE: hypothesis-python/docs/reference/api.rst ================================================ API Reference ============= Reference for non-strategy objects that are part of the Hypothesis API. For documentation on strategies, see the :doc:`strategies reference `. ``@given`` ---------- .. autofunction:: hypothesis.given .. standard #: directives in the source don't work for hypothesis.infer, .. see https://github.com/sphinx-doc/sphinx/issues/6495 .. data:: hypothesis.infer An alias for ``...`` (|ellipsis|). |infer| can be passed to |@given| or |st.builds| to indicate that a strategy for that parameter should be inferred from its type annotations. In all cases, using |infer| is equivalent to using ``...``. Inferred strategies ~~~~~~~~~~~~~~~~~~~ In some cases, Hypothesis can work out what to do when you omit arguments. This is based on introspection, *not* magic, and therefore has well-defined limits. |st.builds| will check the signature of the ``target`` (using :func:`python:inspect.signature`). If there are required arguments with type annotations and no strategy was passed to |st.builds|, |st.from_type| is used to fill them in. You can also pass the value ``...`` (``Ellipsis``) as a keyword argument, to force this inference for arguments with a default value. .. code-block:: pycon >>> def func(a: int, b: str): ... return [a, b] ... >>> builds(func).example() [-6993, ''] |@given| does not perform any implicit inference for required arguments, as this would break compatibility with pytest fixtures. ``...`` (:obj:`python:Ellipsis`), can be used as a keyword argument to explicitly fill in an argument from its type annotation. You can also use the |infer| alias if writing a literal ``...`` seems too weird. .. code:: python @given(a=...) # or @given(a=infer) def test(a: int): pass # is equivalent to @given(a=from_type(int)) def test(a): pass ``@given(...)`` can also be specified to fill all arguments from their type annotations. .. code:: python @given(...) def test(a: int, b: str): pass # is equivalent to @given(a=..., b=...) def test(a, b): pass Limitations ^^^^^^^^^^^ Hypothesis does not inspect :pep:`484` type comments at runtime. While |st.from_type| will work as usual, inference in |st.builds| and |@given| will only work if you manually create the ``__annotations__`` attribute (e.g. by using ``@annotations(...)`` and ``@returns(...)`` decorators). The :mod:`python:typing` module changes between different Python releases, including at minor versions. These are all supported on a best-effort basis, but you may encounter problems. Please report them to us, and consider updating to a newer version of Python as a workaround. Explicit inputs --------------- .. seealso:: See also the :doc:`/tutorial/replaying-failures` tutorial, which discusses using explicit inputs to reproduce failures. .. autoclass:: hypothesis.example .. automethod:: hypothesis.example.xfail .. automethod:: hypothesis.example.via .. _reproducing-inputs: Reproducing inputs ------------------ .. seealso:: See also the :doc:`/tutorial/replaying-failures` tutorial. .. autofunction:: hypothesis.reproduce_failure .. autofunction:: hypothesis.seed Control ------- Functions that can be called from anywhere inside a test, to either modify how Hypothesis treats the current test case, or to give Hypothesis more information about the current test case. .. autofunction:: hypothesis.assume .. autofunction:: hypothesis.note .. autofunction:: hypothesis.event You can mark custom events in a test using |event|: .. code:: python from hypothesis import event, given, strategies as st @given(st.integers().filter(lambda x: x % 2 == 0)) def test_even_integers(i): event(f"i mod 3 = {i%3}") These events appear in :ref:`observability ` output, as well as the output of :ref:`our pytest plugin ` when run with ``--hypothesis-show-statistics``. For instance, in the latter case, you would see output like: .. code-block:: none test_even_integers: - during generate phase (0.09 seconds): - Typical runtimes: < 1ms, ~ 59% in data generation - 100 passing examples, 0 failing examples, 32 invalid examples - Events: * 54.55%, Retried draw from integers().filter(lambda x: x % 2 == 0) to satisfy filter * 31.06%, i mod 3 = 2 * 28.79%, i mod 3 = 0 * 24.24%, Aborted test because unable to satisfy integers().filter(lambda x: x % 2 == 0) * 15.91%, i mod 3 = 1 - Stopped because settings.max_examples=100 Arguments to ``event`` can be any hashable type, but two events will be considered the same if they are the same when converted to a string with :obj:`python:str`. .. _targeted: Targeted property-based testing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Targeted property-based testing combines the advantages of both search-based and property-based testing. Instead of being completely random, targeted PBT uses a search-based component to guide the input generation towards values that have a higher probability of falsifying a property. This explores the input space more effectively and requires fewer tests to find a bug or achieve a high confidence in the system being tested than random PBT. (`Löscher and Sagonas `__) This is not *always* a good idea - for example calculating the search metric might take time better spent running more uniformly-random test cases, or your target metric might accidentally lead Hypothesis *away* from bugs - but if there is a natural metric like "floating-point error", "load factor" or "queue length", we encourage you to experiment with targeted testing. We recommend that users also skim the papers introducing targeted PBT; from `ISSTA 2017 `__ and `ICST 2018 `__. For the curious, the initial implementation in Hypothesis uses hill-climbing search via a mutating fuzzer, with some tactics inspired by simulated annealing to avoid getting stuck and endlessly mutating a local maximum. .. code-block:: python from hypothesis import given, strategies as st, target @given(st.floats(0, 1e100), st.floats(0, 1e100), st.floats(0, 1e100)) def test_associativity_with_target(a, b, c): ab_c = (a + b) + c a_bc = a + (b + c) difference = abs(ab_c - a_bc) target(difference) # Without this, the test almost always passes assert difference < 2.0 .. autofunction:: hypothesis.target Settings -------- .. seealso:: See also :doc:`the tutorial for settings `. .. autoclass:: hypothesis.settings :members: .. autoclass:: hypothesis.Phase :members: .. autoclass:: hypothesis.Verbosity :members: .. autoclass:: hypothesis.HealthCheck :undoc-members: :inherited-members: :exclude-members: all .. _database: Database -------- .. autoclass:: hypothesis.database.ExampleDatabase :members: :private-members: _broadcast_change, _start_listening, _stop_listening .. autoclass:: hypothesis.database.InMemoryExampleDatabase .. autoclass:: hypothesis.database.DirectoryBasedExampleDatabase .. autoclass:: hypothesis.database.GitHubArtifactDatabase .. autoclass:: hypothesis.database.ReadOnlyDatabase .. autoclass:: hypothesis.database.MultiplexedDatabase .. autoclass:: hypothesis.database.BackgroundWriteDatabase .. autoclass:: hypothesis.extra.redis.RedisExampleDatabase .. _stateful: Stateful tests -------------- .. autoclass:: hypothesis.stateful.RuleBasedStateMachine Rules ~~~~~ .. autofunction:: hypothesis.stateful.rule .. autofunction:: hypothesis.stateful.consumes .. autofunction:: hypothesis.stateful.multiple .. autoclass:: hypothesis.stateful.Bundle .. autofunction:: hypothesis.stateful.initialize .. autofunction:: hypothesis.stateful.precondition .. autofunction:: hypothesis.stateful.invariant Running state machines ~~~~~~~~~~~~~~~~~~~~~~ If you want to bypass the TestCase infrastructure you can invoke these manually. The stateful module exposes |run_state_machine_as_test|, which takes an arbitrary function returning a |RuleBasedStateMachine| and an optional settings parameter and does the same as the class based runTest provided. .. autofunction:: hypothesis.stateful.run_state_machine_as_test Hypothesis exceptions --------------------- Custom exceptions raised by Hypothesis. .. autoclass:: hypothesis.errors.HypothesisException .. autoclass:: hypothesis.errors.HypothesisDeprecationWarning .. autoclass:: hypothesis.errors.NonInteractiveExampleWarning .. autoclass:: hypothesis.errors.Flaky .. autoclass:: hypothesis.errors.FlakyStrategyDefinition .. autoclass:: hypothesis.errors.FlakyFailure .. autoclass:: hypothesis.errors.FlakyBackendFailure .. autoclass:: hypothesis.errors.InvalidArgument .. autoclass:: hypothesis.errors.ResolutionFailed .. autoclass:: hypothesis.errors.Unsatisfiable .. autoclass:: hypothesis.errors.DidNotReproduce .. autoclass:: hypothesis.errors.DeadlineExceeded .. _hypothesis-django: Django ------ .. seealso:: See the :ref:`Django strategies reference ` for documentation on strategies in the ``hypothesis.extra.django`` module. Hypothesis offers a number of features specific for Django testing, available in the ``hypothesis[django]`` :doc:`extra `. This is tested against each supported series with mainstream or extended support - if you're still getting security patches, you can test with Hypothesis. .. autoclass:: hypothesis.extra.django.TestCase Using it is quite straightforward: All you need to do is subclass :class:`hypothesis.extra.django.TestCase` or :class:`hypothesis.extra.django.SimpleTestCase` or :class:`hypothesis.extra.django.TransactionTestCase` or :class:`~hypothesis.extra.django.LiveServerTestCase` or :class:`~hypothesis.extra.django.StaticLiveServerTestCase` and you can use |@given| as normal, and the transactions will be per example rather than per test function as they would be if you used |@given| with a normal django test suite (this is important because your test function will be called multiple times and you don't want them to interfere with each other). Test cases on these classes that do not use |@given| will be run as normal for :class:`django:django.test.TestCase` or :class:`django:django.test.TransactionTestCase`. .. autoclass:: hypothesis.extra.django.SimpleTestCase .. autoclass:: hypothesis.extra.django.TransactionTestCase .. autoclass:: hypothesis.extra.django.LiveServerTestCase .. autoclass:: hypothesis.extra.django.StaticLiveServerTestCase We recommend avoiding :class:`~hypothesis.extra.django.TransactionTestCase` unless you really have to run each test case in a database transaction. Because Hypothesis runs this in a loop, the performance problems :class:`django:django.test.TransactionTestCase` normally has are significantly exacerbated and your tests will be really slow. If you are using :class:`~hypothesis.extra.django.TransactionTestCase`, you may need to use ``@settings(suppress_health_check=[HealthCheck.too_slow])`` to avoid a |HealthCheck| error due to slow example generation. Having set up a test class, you can now pass |@given| a strategy for Django models with |django.from_model|. For example, using :gh-file:`the trivial django project we have for testing `: .. code-block:: pycon >>> from hypothesis.extra.django import from_model >>> from toystore.models import Customer >>> c = from_model(Customer).example() >>> c >>> c.email 'jaime.urbina@gmail.com' >>> c.name '\U00109d3d\U000e07be\U000165f8\U0003fabf\U000c12cd\U000f1910\U00059f12\U000519b0\U0003fabf\U000f1910\U000423fb\U000423fb\U00059f12\U000e07be\U000c12cd\U000e07be\U000519b0\U000165f8\U0003fabf\U0007bc31' >>> c.age -873375803 Hypothesis has just created this with whatever the relevant type of data is. Obviously the customer's age is implausible, which is only possible because we have not used (eg) :class:`~django:django.core.validators.MinValueValidator` to set the valid range for this field (or used a :class:`~django:django.db.models.PositiveSmallIntegerField`, which would only need a maximum value validator). If you *do* have validators attached, Hypothesis will only generate examples that pass validation. Sometimes that will mean that we fail a :class:`~hypothesis.HealthCheck` because of the filtering, so let's explicitly pass a strategy to skip validation at the strategy level: .. code-block:: pycon >>> from hypothesis.strategies import integers >>> c = from_model(Customer, age=integers(min_value=0, max_value=120)).example() >>> c >>> c.age 5 Custom field types ~~~~~~~~~~~~~~~~~~ If you have a custom Django field type you can register it with Hypothesis's model deriving functionality by registering a default strategy for it: .. code-block:: pycon >>> from toystore.models import CustomishField, Customish >>> from_model(Customish).example() hypothesis.errors.InvalidArgument: Missing arguments for mandatory field customish for model Customish >>> from hypothesis.extra.django import register_field_strategy >>> from hypothesis.strategies import just >>> register_field_strategy(CustomishField, just("hi")) >>> x = from_model(Customish).example() >>> x.customish 'hi' Note that this mapping is on exact type. Subtypes will not inherit it. Generating child models ~~~~~~~~~~~~~~~~~~~~~~~ For the moment there's no explicit support in hypothesis-django for generating dependent models. i.e. a Company model will generate no Shops. However if you want to generate some dependent models as well, you can emulate this by using the |.flatmap| function as follows: .. code:: python from hypothesis.strategies import just, lists def generate_with_shops(company): return lists(from_model(Shop, company=just(company))).map(lambda _: company) company_with_shops_strategy = from_model(Company).flatmap(generate_with_shops) Let's unpack what this is doing: The way flatmap works is that we draw a value from the original strategy, then apply a function to it which gives us a new strategy. We then draw a value from *that* strategy. So in this case we're first drawing a company, and then we're drawing a list of shops belonging to that company: The |st.just| strategy is a strategy such that drawing it always produces the individual value, so ``from_model(Shop, company=just(company))`` is a strategy that generates a Shop belonging to the original company. So the following code would give us a list of shops all belonging to the same company: .. code:: python from_model(Company).flatmap(lambda c: lists(from_model(Shop, company=just(c)))) The only difference from this and the above is that we want the company, not the shops. This is where the inner map comes in. We build the list of shops and then throw it away, instead returning the company we started for. This works because the models that Hypothesis generates are saved in the database, so we're essentially running the inner strategy purely for the side effect of creating those children in the database. .. _django-generating-primary-key: Generating primary key values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your model includes a custom primary key that you want to generate using a strategy (rather than a default auto-increment primary key) then Hypothesis has to deal with the possibility of a duplicate primary key. If a model strategy generates a value for the primary key field, Hypothesis will create the model instance with :meth:`~django:django.db.models.query.QuerySet.update_or_create`, overwriting any existing instance in the database for this test case with the same primary key. On the subject of ``MultiValueField`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django forms feature the :class:`~django:django.forms.MultiValueField` which allows for several fields to be combined under a single named field, the default example of this is the :class:`~django:django.forms.SplitDateTimeField`. .. code:: python class CustomerForm(forms.Form): name = forms.CharField() birth_date_time = forms.SplitDateTimeField() |django.from_form| supports ``MultiValueField`` subclasses directly, however if you want to define your own strategy be forewarned that Django binds data for a ``MultiValueField`` in a peculiar way. Specifically each sub-field is expected to have its own entry in ``data`` addressed by the field name (e.g. ``birth_date_time``) and the index of the sub-field within the ``MultiValueField``, so form ``data`` for the example above might look like this: .. code:: python { "name": "Samuel John", "birth_date_time_0": "2018-05-19", # the date, as the first sub-field "birth_date_time_1": "15:18:00", # the time, as the second sub-field } Thus, if you want to define your own strategies for such a field you must address your sub-fields appropriately: .. code:: python from_form(CustomerForm, birth_date_time_0=just("2018-05-19")) .. _fuzz_one_input: External fuzzers ---------------- .. autodata:: hypothesis.core.HypothesisHandle.fuzz_one_input .. _custom-function-execution: Custom function execution ------------------------- Hypothesis provides you with a hook that lets you control how it runs examples. This lets you do things like set up and tear down around each example, run examples in a subprocess, transform coroutine tests into normal tests, etc. For example, :class:`~hypothesis.extra.django.TransactionTestCase` in the Django extra runs each example in a separate database transaction. The way this works is by introducing the concept of an executor. An executor is essentially a function that takes a block of code and run it. The default executor is: .. code:: python def default_executor(function): return function() You define executors by defining a method ``execute_example`` on a class. Any test methods on that class with |@given| used on them will use ``self.execute_example`` as an executor with which to run tests. For example, the following executor runs all its code twice: .. code:: python from unittest import TestCase class TestTryReallyHard(TestCase): @given(integers()) def test_something(self, i): perform_some_unreliable_operation(i) def execute_example(self, f): f() return f() Note: The functions you use in map, etc. will run *inside* the executor. i.e. they will not be called until you invoke the function passed to ``execute_example``. An executor must be able to handle being passed a function which returns None, otherwise it won't be able to run normal test cases. So for example the following executor is invalid: .. code:: python from unittest import TestCase class TestRunTwice(TestCase): def execute_example(self, f): return f()() and should be rewritten as: .. code:: python from unittest import TestCase class TestRunTwice(TestCase): def execute_example(self, f): result = f() if callable(result): result = result() return result An alternative hook is provided for use by test runner extensions such as :pypi:`pytest-trio`, which cannot use the ``execute_example`` method. This is **not** recommended for end-users - it is better to write a complete test function directly, perhaps by using a decorator to perform the same transformation before applying |@given|. .. code:: python @given(x=integers()) @pytest.mark.trio async def test(x): ... # Illustrative code, inside the pytest-trio plugin test.hypothesis.inner_test = lambda x: trio.run(test, x) For authors of test runners however, assigning to the ``inner_test`` attribute of the ``hypothesis`` attribute of the test will replace the interior test. .. note:: The new ``inner_test`` must accept and pass through all the ``*args`` and ``**kwargs`` expected by the original test. If the end user has also specified a custom executor using the ``execute_example`` method, it - and all other execution-time logic - will be applied to the *new* inner test assigned by the test runner. Detection --------- .. autofunction:: hypothesis.is_hypothesis_test .. autofunction:: hypothesis.currently_in_test_context ================================================ FILE: hypothesis-python/docs/reference/index.rst ================================================ API Reference ============= The technical API reference for Hypothesis is split into four pages: * :doc:`API Reference `. Non-strategy Hypothesis objects, classes, and functions. |@given| and others live here. * :doc:`Strategies Reference `. Hypothesis strategies, including for :doc:`extras `. * :doc:`Integrations Reference `. Features with a defined interface, but no code API. * :doc:`Hypothesis internals `. Internal APIs for developers building tools, libraries, or research on top of Hypothesis. .. toctree:: :hidden: :maxdepth: 1 api strategies integrations internals ================================================ FILE: hypothesis-python/docs/reference/integrations.rst ================================================ Integrations Reference ====================== Reference for Hypothesis features with a defined interface, but no code API. .. _ghostwriter: Ghostwriter ----------- .. automodule:: hypothesis.extra.ghostwriter :members: A note for test-generation researchers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ghostwritten tests are intended as a *starting point for human authorship*, to demonstrate best practice, help novices past blank-page paralysis, and save time for experts. They *may* be ready-to-run, or include placeholders and ``# TODO:`` comments to fill in strategies for unknown types. In either case, improving tests for their own code gives users a well-scoped and immediately rewarding context in which to explore property-based testing. By contrast, most test-generation tools aim to produce ready-to-run test suites... and implicitly assume that the current behavior is the desired behavior. However, the code might contain bugs, and we want our tests to fail if it does! Worse, tools require that the code to be tested is finished and executable, making it impossible to generate tests as part of the development process. `Fraser 2013`_ found that evolving a high-coverage test suite (e.g. Randoop_, EvoSuite_, Pynguin_) "leads to clear improvements in commonly applied quality metrics such as code coverage [but] no measurable improvement in the number of bugs actually found by developers" and that "generating a set of test cases, even high coverage test cases, does not necessarily improve our ability to test software". Invariant detection (famously Daikon_; in PBT see e.g. `Alonso 2022`_, QuickSpec_, Speculate_) relies on code execution. Program slicing (e.g. FUDGE_, FuzzGen_, WINNIE_) requires downstream consumers of the code to test. Ghostwriter inspects the function name, argument names and types, and docstrings. It can be used on buggy or incomplete code, runs in a few seconds, and produces a single semantically-meaningful test per function or group of functions. Rather than detecting regressions, these tests check semantic properties such as `encode/decode or save/load round-trips `__, for `commutative, associative, and distributive operations `__, `equivalence between methods `__, `array shapes `__, and idempotence. Where no property is detected, we simply check for 'no error on valid input' and allow the user to supply their own invariants. Evaluations such as the SBFT24_ competition_ measure performance on a task which the Ghostwriter is not intended to perform. I'd love to see qualitative user studies, such as `PBT in Practice`_ for test generation, which could check whether the Ghostwriter is onto something or tilting at windmills. If you're interested in similar questions, `drop me an email`_! .. _Daikon: https://plse.cs.washington.edu/daikon/pubs/ .. _Alonso 2022: https://doi.org/10.1145/3540250.3559080 .. _QuickSpec: http://www.cse.chalmers.se/~nicsma/papers/quickspec2.pdf .. _Speculate: https://matela.com.br/speculate.pdf .. _FUDGE: https://research.google/pubs/pub48314/ .. _FuzzGen: https://www.usenix.org/conference/usenixsecurity20/presentation/ispoglou .. _WINNIE: https://www.ndss-symposium.org/wp-content/uploads/2021-334-paper.pdf .. _Fraser 2013: https://doi.org/10.1145/2483760.2483774 .. _Randoop: https://homes.cs.washington.edu/~mernst/pubs/feedback-testgen-icse2007.pdf .. _EvoSuite: https://www.evosuite.org/wp-content/papercite-data/pdf/esecfse11.pdf .. _Pynguin: https://arxiv.org/abs/2007.14049 .. _SBFT24: https://arxiv.org/abs/2401.15189 .. _competition: https://github.com/ThunderKey/python-tool-competition-2024 .. _PBT in Practice: https://harrisongoldste.in/papers/icse24-pbt-in-practice.pdf .. _drop me an email: mailto:zac@zhd.dev?subject=Hypothesis%20Ghostwriter%20research .. _observability: Observability ------------- .. note:: The `Tyche `__ VSCode extension provides an in-editor UI for observability results generated by Hypothesis. If you want to *view* observability results, rather than programmatically consume or display them, we recommend using Tyche. .. warning:: This feature is experimental, and could have breaking changes or even be removed without notice. Try it out, let us know what you think, but don't rely on it just yet! Motivation ~~~~~~~~~~ Understanding what your code is doing - for example, why your test failed - is often a frustrating exercise in adding some more instrumentation or logging (or ``print()`` calls) and running it again. The idea of :wikipedia:`observability ` is to let you answer questions you didn't think of in advance. In slogan form, *Debugging should be a data analysis problem.* By default, Hypothesis only reports the minimal failing example... but sometimes you might want to know something about *all* the examples. Printing them to the terminal by increasing |Verbosity| might be nice, but isn't always enough. This feature gives you an analysis-ready dataframe with useful columns and one row per test case, with columns from arguments to code coverage to pass/fail status. This is deliberately a much lighter-weight and task-specific system than e.g. `OpenTelemetry `__. It's also less detailed than time-travel debuggers such as `rr `__ or `pytrace `__, because there's no good way to compare multiple traces from these tools and their Python support is relatively immature. Configuration ~~~~~~~~~~~~~ If you set the ``HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY`` environment variable, Hypothesis will log various observations to jsonlines files in the ``.hypothesis/observed/`` directory. You can load and explore these with e.g. :func:`pd.read_json(".hypothesis/observed/*_testcases.jsonl", lines=True) `, or by using the :pypi:`sqlite-utils` and :pypi:`datasette` libraries:: sqlite-utils insert testcases.db testcases .hypothesis/observed/*_testcases.jsonl --nl --flatten datasette serve testcases.db If you are experiencing a significant slow-down, you can try setting ``HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY_NOCOVER`` instead; this will disable coverage information collection. This should not be necessary on Python 3.12 or later, where coverage collection is very fast. Collecting more information ^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you want to record more information about your test cases than the arguments and outcome - for example, was ``x`` a binary tree? what was the difference between the expected and the actual value? how many queries did it take to find a solution? - Hypothesis makes this easy. :func:`~hypothesis.event` accepts a string label, and optionally a string or int or float observation associated with it. All events are collected and summarized in :ref:`statistics`, as well as included on a per-test-case basis in our observations. :func:`~hypothesis.target` is a special case of numeric-valued events: as well as recording them in observations, Hypothesis will try to maximize the targeted value. Knowing that, you can use this to guide the search for failing inputs. Data Format ~~~~~~~~~~~ We dump observations in `json lines format `__, with each line describing either a test case or an information message. The tables below are derived from :download:`this machine-readable JSON schema `, to provide both readable and verifiable specifications. Note that we use :func:`python:json.dumps` and can therefore emit non-standard JSON which includes infinities and NaN. This is valid in `JSON5 `__, and supported by `some JSON parsers `__ including Gson in Java, ``JSON.parse()`` in Ruby, and of course in Python. Information message ^^^^^^^^^^^^^^^^^^^ .. jsonschema:: ./schema_observations.json#/oneOf/1 :hide_key: /additionalProperties, /type Test case ^^^^^^^^^ .. jsonschema:: ./schema_observations.json#/oneOf/0 :hide_key: /additionalProperties, /type .. _observability-hypothesis-metadata: Hypothesis metadata +++++++++++++++++++ While the observability format is agnostic to the property-based testing library which generated it, Hypothesis includes specific values in the ``metadata`` key for test cases. You may rely on these being present if and only if the observation was generated by Hypothesis. .. jsonschema:: ./schema_metadata.json :hide_key: /additionalProperties, /type Choices metadata ++++++++++++++++ These additional metadata elements are included in ``metadata`` (as e.g. ``metadata["choice_nodes"]`` or ``metadata["choice_spans"]``), if and only if |OBSERVABILITY_CHOICES| is set. .. jsonschema:: ./schema_metadata_choices.json :hide_key: /additionalProperties, /type .. _pytest-plugin: The Hypothesis pytest plugin ---------------------------- Hypothesis includes a tiny plugin to improve integration with :pypi:`pytest`, which is activated by default (but does not affect other test runners). It aims to improve the integration between Hypothesis and Pytest by providing extra information and convenient access to config options. - ``pytest --hypothesis-show-statistics`` can be used to :ref:`display test and data generation statistics `. - ``pytest --hypothesis-profile=`` can be used to load a settings profile (as in |settings.load_profile|). - ``pytest --hypothesis-verbosity=`` can be used to override the current |Verbosity| setting. - ``pytest --hypothesis-seed=`` can be used to reproduce a failure with a particular seed (as in |@seed|). - ``pytest --hypothesis-explain`` can be used to temporarily enable |Phase.explain|. Finally, all tests that are defined with Hypothesis automatically have ``@pytest.mark.hypothesis`` applied to them. See :ref:`here for information on working with markers `. .. note:: Pytest will load the plugin automatically if Hypothesis is installed. You don't need to do anything at all to use it. If this causes problems, you can avoid loading the plugin with the ``-p no:hypothesispytest`` option. .. _statistics: Test statistics ~~~~~~~~~~~~~~~ .. note:: While test statistics are only available under pytest, you can use the :ref:`observability ` interface to view similar information about your tests. You can see a number of statistics about executed tests by passing the command line argument ``--hypothesis-show-statistics``. This will include some general statistics about the test: For example if you ran the following with ``--hypothesis-show-statistics``: .. code-block:: python from hypothesis import given, strategies as st @given(st.integers()) def test_integers(i): pass You would see: .. code-block:: none - during generate phase (0.06 seconds): - Typical runtimes: < 1ms, ~ 47% in data generation - 100 passing examples, 0 failing examples, 0 invalid examples - Stopped because settings.max_examples=100 The final "Stopped because" line tells you why Hypothesis stopped generating new examples. This is typically because we hit |max_examples|, but occasionally because we exhausted the search space or because shrinking was taking a very long time. This can be useful for understanding the behaviour of your tests. In some cases (such as filtered and recursive strategies) you will see events mentioned which describe some aspect of the data generation: .. code-block:: python from hypothesis import given, strategies as st @given(st.integers().filter(lambda x: x % 2 == 0)) def test_even_integers(i): pass You would see something like: .. code-block:: none test_even_integers: - during generate phase (0.08 seconds): - Typical runtimes: < 1ms, ~ 57% in data generation - 100 passing examples, 0 failing examples, 12 invalid examples - Events: * 51.79%, Retried draw from integers().filter(lambda x: x % 2 == 0) to satisfy filter * 10.71%, Aborted test because unable to satisfy integers().filter(lambda x: x % 2 == 0) - Stopped because settings.max_examples=100 .. _hypothesis-cli: hypothesis[cli] ---------------- .. note:: This feature requires the ``hypothesis[cli]`` :doc:`extra `, via ``pip install hypothesis[cli]``. .. automodule:: hypothesis.extra.cli .. _codemods: hypothesis[codemods] -------------------- .. note:: This feature requires the ``hypothesis[codemods]`` :doc:`extra `, via ``pip install hypothesis[codemods]``. .. automodule:: hypothesis.extra.codemods .. _hypothesis-dpcontracts: hypothesis[dpcontracts] ----------------------- .. note:: This feature requires the ``hypothesis[dpcontracts]`` :doc:`extra `, via ``pip install hypothesis[dpcontracts]``. .. tip:: For new projects, we recommend using either :pypi:`deal` or :pypi:`icontract` and :pypi:`icontract-hypothesis` over :pypi:`dpcontracts`. They're generally more powerful tools for design-by-contract programming, and have substantially nicer Hypothesis integration too! .. automodule:: hypothesis.extra.dpcontracts :members: ================================================ FILE: hypothesis-python/docs/reference/internals.rst ================================================ Hypothesis internals ==================== .. warning:: This page documents internal Hypothesis interfaces. Some are fairly stable, while others are still experimental. In either case, they are not subject to our standard :ref:`deprecation policy `, and we might make breaking changes in minor or patch releases. This page is intended for people building tools, libraries, or research on top of Hypothesis. If that includes you, please get in touch! We'd love to hear what you're doing, or explore more stable ways to support your use-case. .. _alternative-backends-internals: Alternative backends -------------------- .. seealso:: See also the user-facing :ref:`alternative-backends` documentation. .. autoclass:: hypothesis.internal.conjecture.providers.PrimitiveProvider :members: .. autodata:: hypothesis.internal.conjecture.providers.AVAILABLE_PROVIDERS :no-value: .. autofunction:: hypothesis.internal.conjecture.provider_conformance.run_conformance_test .. autoclass:: hypothesis.errors.BackendCannotProceed .. autoclass:: hypothesis.internal.intervalsets.IntervalSet Observability ------------- .. autofunction:: hypothesis.internal.observability.add_observability_callback .. autofunction:: hypothesis.internal.observability.remove_observability_callback .. autofunction:: hypothesis.internal.observability.with_observability_callback .. autofunction:: hypothesis.internal.observability.observability_enabled .. autodata:: hypothesis.internal.observability.TESTCASE_CALLBACKS .. autodata:: hypothesis.internal.observability.OBSERVABILITY_COLLECT_COVERAGE .. autodata:: hypothesis.internal.observability.OBSERVABILITY_CHOICES Engine constants ---------------- We pick reasonable values for these constants, but if you must, you can monkeypatch them. (Hypothesis is not responsible for any performance degradation that may result). .. autodata:: hypothesis.internal.conjecture.engine.MAX_SHRINKS .. autodata:: hypothesis.internal.conjecture.engine.MAX_SHRINKING_SECONDS .. autodata:: hypothesis.internal.conjecture.engine.BUFFER_SIZE ================================================ FILE: hypothesis-python/docs/reference/schema_metadata.json ================================================ { "type": "object", "properties": { "traceback": { "type": ["string", "null"], "description": "The traceback for failing tests, if and only if ``status == \"failed\"``." }, "reproduction_decorator": { "type": ["string", "null"], "description": "The ``@reproduce_failure`` decorator string for failing tests, if and only if ``status == \"failed\"``." }, "predicates": { "type": "object", "description": "The number of times each |assume| and |@precondition| predicate was satisfied (``True``) and not satisfied (``False``).", "additionalProperties": { "type": "object", "properties": { "satisfied": { "type": "integer", "minimum": 0, "description": "The number of times this predicate was satisfied (``True``)." }, "unsatisfied": { "type": "integer", "minimum": 0, "description": "The number of times this predicate was not satisfied (``False``)." } }, "required": ["satisfied", "unsatisfied"], "additionalProperties": false } }, "backend": { "type": "object", "description": "Backend-specific observations from |PrimitiveProvider.observe_test_case| and |PrimitiveProvider.observe_information_messages|." }, "sys.argv": { "type": "array", "items": {"type": "string"}, "description": "The result of ``sys.argv``." }, "os.getpid()": { "type": "integer", "description": "The result of ``os.getpid()``." }, "imported_at": { "type": "number", "description": "The unix timestamp when Hypothesis was imported." }, "phase": { "type": "string", "description": "The Hypothesis |Phase| this test case was generated in." }, "data_status": { "type": "number", "enum": [0, 1, 2, 3], "description": "The internal status of the ConjectureData for this test case. The values are as follows: ``Status.OVERRUN = 0``, ``Status.INVALID = 1``, ``Status.VALID = 2``, and ``Status.INTERESTING = 3``." }, "interesting_origin": { "type": ["string", "null"], "description": "The internal ``InterestingOrigin`` object for failing tests, if and only if ``status == \"failed\"``. The ``traceback`` string value is derived from this object." } }, "required": ["traceback", "reproduction_decorator", "predicates", "backend", "sys.argv", "os.getpid()", "imported_at", "data_status", "interesting_origin"], "additionalProperties": false } ================================================ FILE: hypothesis-python/docs/reference/schema_metadata_choices.json ================================================ { "type": "object", "properties": { "choice_nodes": { "type": ["array", "null"], "description": ".. warning::\n\n EXPERIMENTAL AND UNSTABLE. This attribute may change format or disappear without warning.\n\nThe sequence of choices made during this test case. This includes the choice value, as well as its constraints and whether it was forced or not.\n\nOnly present if |OBSERVABILITY_CHOICES| is ``True``.\n\n.. note::\n\n The choice sequence is a relatively low-level implementation detail of Hypothesis, and is exposed in observability for users building tools or research on top of Hypothesis. See |PrimitiveProvider| for more details about the choice sequence.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": ["integer", "float", "string", "bytes", "boolean"], "description": "The type of choice made. Corresponds to a call to |PrimitiveProvider.draw_integer|, |PrimitiveProvider.draw_float|, |PrimitiveProvider.draw_string|, |PrimitiveProvider.draw_bytes|, or |PrimitiveProvider.draw_boolean|." }, "value": { "description": "The value of the choice. Corresponds to the value returned by a ``PrimitiveProvider.draw_*`` method.\n\n``NaN`` float values are returned as ``[\"float\", ]``, to distinguish ``NaN`` floats with nonstandard bit patterns. Integers with ``abs(value) >= 2**63`` are returned as ``[\"integer\", str(value)]``, for compatibility with tools with integer size limitations. Bytes are returned as ``[\"bytes\", base64.b64encode(value)]``." }, "constraints": { "type": "object", "description": "The constraints for this choice. Corresponds to the constraints passed to a ``PrimitiveProvider.draw_*`` method. ``NaN`` float values, integers with ``abs(value) >= 2**63``, and byte values for constraints are transformed as for the ``value`` attribute." }, "was_forced": { "type": "boolean", "description": "Whether this choice was forced. As an implementation detail, Hypothesis occasionally requires that some choices take on a specific value, for instance to end generation of collection elements early for performance. These values are called \"forced\", and have ``was_forced = True``." } }, "required": ["type", "value", "constraints", "was_forced"], "additionalProperties": false } }, "choice_spans": { "type": "array", "items": {"type": "array"}, "description": ".. warning::\n\n EXPERIMENTAL AND UNSTABLE. This attribute may change format or disappear without warning.\n\nThe semantically-meaningful spans of the choice sequence of this test case.\n\nEach span has the format ``[label, start, end, discarded]``, where:\n\n* ``label`` is an opaque integer-value string shared by all spans drawn from a particular strategy.\n* ``start`` and ``end`` are indices into the choice sequence for this span, such that ``choices[start:end]`` are the corresponding choices.\n* ``discarded`` is a boolean indicating whether this span was discarded (see |PrimitiveProvider.span_end|).\n\nOnly present if |OBSERVABILITY_CHOICES| is ``True``.\n\n.. note::\n\n Spans are a relatively low-level implementation detail of Hypothesis, and are exposed in observability for users building tools or research on top of Hypothesis. See |PrimitiveProvider| (and particularly |PrimitiveProvider.span_start| and |PrimitiveProvider.span_end|) for more details about spans." } }, "required": ["traceback", "reproduction_decorator", "predicates", "backend", "sys.argv", "os.getpid()", "imported_at", "data_status", "interesting_origin", "choice_nodes", "choice_spans"], "additionalProperties": false } ================================================ FILE: hypothesis-python/docs/reference/schema_observations.json ================================================ { "title": "PBT Observations", "description": "PBT Observations define a standard way to communicate what happened when property-based tests were run. They describe test cases, or general notifications classified as info, alert, or error messages.", "oneOf": [ { "description": "Describes the inputs to and result of running some test function on a particular input. The test might have passed, failed, or been abandoned part way through (e.g. because we failed a |.filter| condition).", "type": "object", "properties": { "type": { "const": "test_case", "description": "A tag which labels this observation as data about a specific test case." }, "status": { "enum": ["passed", "failed", "gave_up"], "description": "Whether the test passed, failed, or was aborted before completion (e.g. due to use of |.filter|). Note that if we gave_up partway, values such as arguments and features may be incomplete." }, "status_reason": { "type": "string", "description": "If non-empty, the reason for which the test failed or was abandoned. For Hypothesis, this is usually the exception type and location." }, "representation": { "type": "string", "description": "The string representation of the input. In Hypothesis, this includes the property name and arguments (like ``test_a(a=1)``), any interactive draws from |st.data|, and additionally some comments from |Phase.explain| for failing examples." }, "arguments": { "type": "object", "description": "A structured json-encoded representation of the input. Hypothesis provides a dictionary of argument names to json-ified values, including interactive draws from the |st.data| strategy. If 'status' is 'gave_up', this may be absent or incomplete. In other libraries this can be any object." }, "how_generated": { "type": ["string", "null"], "description": "How the input was generated, if known. In Hypothesis this might be an explicit example, generated during a particular phase with some backend, or by replaying the minimal failing example." }, "features": { "type": "object", "description": "Runtime observations which might help explain what this test case did. Hypothesis includes |target| scores, tags from |event|, and so on." }, "coverage": { "type": ["object", "null"], "description": "Mapping of filename to list of covered line numbers, if coverage information is available, or None if not. Hypothesis deliberately omits stdlib and site-packages code.", "additionalProperties": { "type": "array", "items": {"type": "integer", "minimum": 1}, "uniqueItems": true } }, "timing": { "type": "object", "description": "The time in seconds taken by non-overlapping parts of this test case. Hypothesis reports ``execute:test``, ``overall:gc``, and ``generate:{argname}`` for each argument.", "additionalProperties": { "type": "number", "minimum": 0 } }, "metadata": { "type": "object", "description": "Arbitrary metadata which might be of interest, but does not semantically fit in 'features'. For example, Hypothesis includes the traceback for failing tests here." }, "property": { "type": "string", "description": "The name or representation of the test function we're running." }, "run_start": { "type": "number", "description": "unix timestamp at which we started running this test function, so that later analysis can group test cases by run." } }, "required": ["type", "status", "status_reason", "representation", "arguments", "how_generated", "features", "coverage", "timing", "metadata", "property", "run_start"], "additionalProperties": false }, { "description": "Info, alert, and error messages correspond to a group of test cases or the overall run, and are intended for humans rather than machine analysis.", "type": "object", "properties": { "type": { "enum": ["info", "alert", "error"], "description": "A tag which labels this observation as general information to show the user. Hypothesis uses info messages to report statistics; alert or error messages can be provided by plugins." }, "title": { "type": "string", "description": "The title of this message" }, "content": { "type": ["string", "object"], "description": "The body of the message. Strings are presumed to be human-readable messages in markdown format; dictionaries may contain arbitrary information (as for test-case metadata)." }, "property": { "type": "string", "description": "The name or representation of the test function we're running. For Hypothesis, usually the Pytest nodeid." }, "run_start": { "type": "number", "description": "unix timestamp at which we started running this test function, so that later analysis can group test cases by run." } }, "required": ["type", "title", "content", "property", "run_start"], "additionalProperties": false } ] } ================================================ FILE: hypothesis-python/docs/reference/strategies.rst ================================================ .. _strategies: Strategies Reference ==================== Strategies are the way Hypothesis describes the values for |@given| to generate. For instance, passing the strategy ``st.lists(st.integers(), min_size=1)`` to |@given| tells Hypothesis to generate lists of integers with at least one element. This reference page lists all of Hypothesis' first-party functions which return a strategy. There are also many provided by :doc:`third-party libraries `. Note that we often say "strategy" when we mean "function returning a strategy"; it's usually clear from context which one we mean. Strategies can be passed to other strategies as arguments, combined using :ref:`combinator strategies `, or modified using |.filter|, |.map|, or |.flatmap|. Primitives ---------- .. autofunction:: hypothesis.strategies.none .. autofunction:: hypothesis.strategies.nothing .. autofunction:: hypothesis.strategies.just .. autofunction:: hypothesis.strategies.booleans Numeric ------- .. seealso:: See also the separate sections for :ref:`Numpy strategies `, :ref:`Pandas strategies `, and :ref:`Array API strategies `. .. autofunction:: hypothesis.strategies.integers .. autofunction:: hypothesis.strategies.floats .. autofunction:: hypothesis.strategies.complex_numbers .. autofunction:: hypothesis.strategies.decimals .. autofunction:: hypothesis.strategies.fractions Strings ------- .. note:: The |st.uuids| and |st.ip_addresses| strategies generate instances of :mod:`UUID ` and :mod:`IPAddress ` respectively. You can generate corresponding string values by using |.map|, such as ``st.uuids().map(str)``. .. autofunction:: hypothesis.strategies.text .. autofunction:: hypothesis.strategies.characters .. autofunction:: hypothesis.strategies.from_regex .. autofunction:: hypothesis.strategies.binary .. autofunction:: hypothesis.strategies.emails .. autofunction:: hypothesis.provisional.domains .. warning:: The |st.domains| strategy is provisional. Its interface may be changed in a minor release, without being subject to our :ref:`deprecation policy `. That said, we expect it to be relatively stable. .. autofunction:: hypothesis.provisional.urls .. warning:: The |st.urls| strategy is provisional. Its interface may be changed in a minor release, without being subject to our :ref:`deprecation policy `. That said, we expect it to be relatively stable. Collections ----------- .. autofunction:: hypothesis.strategies.lists .. autofunction:: hypothesis.strategies.tuples .. autofunction:: hypothesis.strategies.sets .. autofunction:: hypothesis.strategies.frozensets .. autofunction:: hypothesis.strategies.dictionaries .. autofunction:: hypothesis.strategies.fixed_dictionaries .. autofunction:: hypothesis.strategies.iterables Datetime -------- .. autofunction:: hypothesis.strategies.dates .. autofunction:: hypothesis.strategies.times .. autofunction:: hypothesis.strategies.datetimes .. autofunction:: hypothesis.strategies.timezones .. autofunction:: hypothesis.strategies.timezone_keys .. autofunction:: hypothesis.strategies.timedeltas Recursive --------- .. autofunction:: hypothesis.strategies.recursive .. autofunction:: hypothesis.strategies.deferred Random ------ .. autofunction:: hypothesis.strategies.randoms .. autofunction:: hypothesis.strategies.random_module .. autofunction:: hypothesis.register_random .. _combinators: Combinators ----------- .. autofunction:: hypothesis.strategies.one_of .. autofunction:: hypothesis.strategies.builds .. autofunction:: hypothesis.strategies.composite .. autofunction:: hypothesis.strategies.data Typing ------ .. autofunction:: hypothesis.strategies.from_type .. autofunction:: hypothesis.strategies.register_type_strategy Hypothesis ---------- .. autofunction:: hypothesis.strategies.runner .. autofunction:: hypothesis.strategies.shared Misc ---- .. autofunction:: hypothesis.strategies.functions .. autofunction:: hypothesis.strategies.slices .. autofunction:: hypothesis.strategies.uuids .. autofunction:: hypothesis.strategies.ip_addresses .. autofunction:: hypothesis.strategies.sampled_from .. autofunction:: hypothesis.strategies.permutations Related ------- .. autoclass:: hypothesis.strategies.DrawFn .. autoclass:: hypothesis.strategies.DataObject .. automethod:: hypothesis.strategies.DataObject.draw .. autoclass:: hypothesis.strategies.SearchStrategy .. automethod:: hypothesis.strategies.SearchStrategy.example .. automethod:: hypothesis.strategies.SearchStrategy.filter .. automethod:: hypothesis.strategies.SearchStrategy.map .. automethod:: hypothesis.strategies.SearchStrategy.flatmap .. _hypothesis-numpy: NumPy ----- Hypothesis offers a number of strategies for `NumPy `_ testing, available in the ``hypothesis[numpy]`` :doc:`extra `. It lives in the ``hypothesis.extra.numpy`` package. The centerpiece is the :func:`~hypothesis.extra.numpy.arrays` strategy, which generates arrays with any dtype, shape, and contents you can specify or give a strategy for. To make this as useful as possible, strategies are provided to generate array shapes and generate all kinds of fixed-size or compound dtypes. .. automodule:: hypothesis.extra.numpy :members: :exclude-members: ArrayStrategy, BasicIndexStrategy, BroadcastableShapes, MutuallyBroadcastableShapesStrategy .. _hypothesis-pandas: pandas ------ Hypothesis provides strategies for several of the core pandas data types: :class:`pandas.Index`, :class:`pandas.Series` and :class:`pandas.DataFrame`. The general approach taken by the pandas module is that there are multiple strategies for generating indexes, and all of the other strategies take the number of entries they contain from their index strategy (with sensible defaults). So e.g. a Series is specified by specifying its :class:`numpy.dtype` (and/or a strategy for generating elements for it). .. automodule:: hypothesis.extra.pandas :members: Supported versions ~~~~~~~~~~~~~~~~~~ There is quite a lot of variation between pandas versions. We only commit to supporting the latest version of pandas, but older minor versions are supported on a "best effort" basis. Hypothesis is currently tested against and confirmed working with every Pandas minor version from 1.1 through to 2.2. Releases that are not the latest patch release of their minor version are not tested or officially supported, but will probably also work unless you hit a pandas bug. .. _array-api: Array API --------- .. note:: Several array libraries have more library-specific strategies, including :pypi:`Xarray ` (via their :ref:`upstream strategies `) and :pypi:`NumPy` (via :ref:`its Hypothesis extra `). Of course, strategies in the Array API namespace can still be used to test Xarray or NumPy, just like any other array library. Hypothesis offers strategies for `Array API `_ adopting libraries in the ``hypothesis.extra.array_api`` package. See :issue:`3037` for more details. If you want to test with :pypi:`CuPy `, :pypi:`Dask `, :pypi:`JAX `, :pypi:`MXNet `, :pypi:`PyTorch `, :pypi:`TensorFlow `, or :pypi:`Xarray ` - or just :pypi:`NumPy ` - this is the extension for you! .. autofunction:: hypothesis.extra.array_api.make_strategies_namespace The resulting namespace contains all our familiar strategies like :func:`~xps.arrays` and :func:`~xps.from_dtype`, but based on the Array API standard semantics and returning objects from the ``xp`` module: .. automodule:: xps :members: from_dtype, arrays, array_shapes, scalar_dtypes, boolean_dtypes, numeric_dtypes, real_dtypes, integer_dtypes, unsigned_integer_dtypes, floating_dtypes, complex_dtypes, valid_tuple_axes, broadcastable_shapes, mutually_broadcastable_shapes, indices, .. _django-strategies: Django ------ .. seealso:: See the :ref:`Django API reference ` for documentation on testing Django with Hypothesis. .. autofunction:: hypothesis.extra.django.from_model .. autofunction:: hypothesis.extra.django.from_form .. autofunction:: hypothesis.extra.django.from_field .. autofunction:: hypothesis.extra.django.register_field_strategy .. _hypothesis-lark: hypothesis[lark] ---------------- .. note:: Strategies in this module require the ``hypothesis[lark]`` :doc:`extra `, via ``pip install hypothesis[lark]``. .. automodule:: hypothesis.extra.lark :members: Example grammars, which may provide a useful starting point for your tests, can be found `in the Lark repository `__ and in `this third-party collection `__. .. _hypothesis-pytz: hypothesis[pytz] ---------------- .. note:: Strategies in this module require the ``hypothesis[pytz]`` :doc:`extra `, via ``pip install hypothesis[pytz]``. .. automodule:: hypothesis.extra.pytz :members: .. _hypothesis-dateutil: hypothesis[dateutil] -------------------- .. note:: Strategies in this module require the ``hypothesis[dateutil]`` :doc:`extra `, via ``pip install hypothesis[dateutil]``. .. automodule:: hypothesis.extra.dateutil :members: ================================================ FILE: hypothesis-python/docs/stateful.rst ================================================ Stateful tests ============== .. note:: See also `How not to Die Hard with Hypothesis `__ and `An Introduction to Rule-Based Stateful Testing `__. With |@given|, your tests are still something that you mostly write yourself, with Hypothesis providing some data. With Hypothesis's *stateful testing*, Hypothesis instead tries to generate not just data but entire tests. You specify a number of primitive actions that can be combined together, and then Hypothesis will try to find sequences of those actions that result in a failure. You may not need stateful tests ------------------------------- The basic idea of stateful testing is to make Hypothesis choose actions as well as values for your test, and state machines are a great declarative way to do just that. For simpler cases though, you might not need them at all - a standard test with |@given| might be enough, since you can use |st.data| in branches or loops. In fact, that's how the state machine explorer works internally. For more complex workloads though, where a higher level API comes into it's own, keep reading! Rule-based state machines ------------------------- A state machine is very similar to a normal |@given| based test in that it takes values drawn from strategies and passes them to a user defined test function, which may use assertions to check the system's behavior. The key difference is that where |@given| based tests must be independent, rules can be chained together - a single test run may involve multiple rule invocations, which may interact in various ways. Rules can take normal strategies as arguments, but normal strategies, with the exception of :func:`~hypothesis.strategies.runner` and |st.data|, cannot take into account the current state of the machine. This is where bundles come in. A rule can, in place of a normal strategy, take a :class:`~hypothesis.stateful.Bundle`. A :class:`hypothesis.stateful.Bundle` is a named collection of generated values that can be reused by other operations in the test. They are populated with the results of rules, and may be used as arguments to rules, allowing data to flow from one rule to another, and rules to work on the results of previous computations or actions. Specifically, a rule that specifies ``target=a_bundle`` will cause its return value to be added to that bundle. A rule that specifies ``an_argument=a_bundle`` as a strategy will draw a value from that bundle. A rule can also specify that an argument chooses a value from a bundle and removes that value by using :func:`~hypothesis.stateful.consumes` as in ``an_argument=consumes(a_bundle)``. .. note:: There is some overlap between what you can do with Bundles and what you can do with instance variables. Both represent state that rules can manipulate. If you do not need to draw values that depend on the machine's state, you can simply use instance variables. If you do need to draw values that depend on the machine's state, Bundles provide a fairly straightforward way to do this. If you need rules that draw values that depend on the machine's state in some more complicated way, you will have to abandon bundles. You can use :func:`~hypothesis.strategies.runner` and |.flatmap| to access the instance from a rule: the strategy ``runner().flatmap(lambda self: sampled_from(self.a_list))`` will draw from the instance variable ``a_list``. If you need something more complicated still, you can use |st.data| to draw data from the instance (or anywhere else) based on logic in the rule. The following rule based state machine example is a simplified version of a test for Hypothesis's example database implementation. An example database maps keys to sets of values, and in this test we compare one implementation of it to a simplified in memory model of its behaviour, which just stores the same values in a Python ``dict``. The test then runs operations against both the real database and the in-memory representation of it and looks for discrepancies in their behaviour. .. code:: python import shutil import tempfile from collections import defaultdict import hypothesis.strategies as st from hypothesis.database import DirectoryBasedExampleDatabase from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule class DatabaseComparison(RuleBasedStateMachine): def __init__(self): super().__init__() self.tempd = tempfile.mkdtemp() self.database = DirectoryBasedExampleDatabase(self.tempd) self.model = defaultdict(set) keys = Bundle("keys") values = Bundle("values") @rule(target=keys, k=st.binary()) def add_key(self, k): return k @rule(target=values, v=st.binary()) def add_value(self, v): return v @rule(k=keys, v=values) def save(self, k, v): self.model[k].add(v) self.database.save(k, v) @rule(k=keys, v=values) def delete(self, k, v): self.model[k].discard(v) self.database.delete(k, v) @rule(k=keys) def values_agree(self, k): assert set(self.database.fetch(k)) == self.model[k] def teardown(self): shutil.rmtree(self.tempd) TestDBComparison = DatabaseComparison.TestCase In this we declare two bundles - one for keys, and one for values. We have two trivial rules which just populate them with data (``k`` and ``v``), and three non-trivial rules: ``save`` saves a value under a key and ``delete`` removes a value from a key, in both cases also updating the model of what *should* be in the database. ``values_agree`` then checks that the contents of the database agrees with the model for a particular key. .. note:: While this could have been simplified by not using bundles, generating keys and values directly in the ``save`` and ``delete`` rules, using bundles encourages Hypothesis to choose the same keys and values for multiple operations. The bundle operations establish a "universe" of keys and values that are used in the rules. We can now integrate this into our test suite by getting a unittest TestCase from it: .. code:: python TestTrees = DatabaseComparison.TestCase # Or just run with pytest's unittest support if __name__ == "__main__": unittest.main() This test currently passes, but if we comment out the line where we call ``self.model[k].discard(v)``, we would see the following output when run under pytest:: AssertionError: assert set() == {b''} ------------ Hypothesis ------------ state = DatabaseComparison() var1 = state.add_key(k=b'') var2 = state.add_value(v=var1) state.save(k=var1, v=var2) state.delete(k=var1, v=var2) state.values_agree(k=var1) state.teardown() Note how it's printed out a very short program that will demonstrate the problem. The output from a rule based state machine should generally be pretty close to Python code - if you have custom ``repr`` implementations that don't return valid Python then it might not be, but most of the time you should just be able to copy and paste the code into a test to reproduce it. You can control the detailed behaviour with a settings object on the TestCase (this is a normal hypothesis settings object using the defaults at the time the TestCase class was first referenced). For example if you wanted to run fewer examples with larger programs you could change the settings to: .. code:: python DatabaseComparison.TestCase.settings = settings( max_examples=50, stateful_step_count=100 ) Which doubles the number of steps each program runs and halves the number of test cases that will be run. Rules ----- As said earlier, rules are the most common feature used in RuleBasedStateMachine. They are defined by applying the :func:`~hypothesis.stateful.rule` decorator on a function. Note that RuleBasedStateMachine must have at least one rule defined and that a single function cannot be used to define multiple rules (this is to avoid having multiple rules doing the same things). Due to the stateful execution method, rules generally cannot take arguments from other sources such as fixtures or ``pytest.mark.parametrize`` - consider providing them via a strategy such as :func:`~hypothesis.strategies.sampled_from` instead. Initializes ----------- Initializes are a special case of rules, which are guaranteed to be run exactly once before any normal rule is called. Note if multiple initialize rules are defined, they will all be called but in any order, and that order will vary from run to run. Initializes are typically useful to populate bundles: .. code:: python import hypothesis.strategies as st from hypothesis.stateful import Bundle, RuleBasedStateMachine, initialize, rule name_strategy = st.text(min_size=1).filter(lambda x: "/" not in x) class NumberModifier(RuleBasedStateMachine): folders = Bundle("folders") files = Bundle("files") @initialize(target=folders) def init_folders(self): return "/" @rule(target=folders, parent=folders, name=name_strategy) def create_folder(self, parent, name): return f"{parent}/{name}" @rule(target=files, parent=folders, name=name_strategy) def create_file(self, parent, name): return f"{parent}/{name}" Initializes can also allow you to initialize the system under test in a way that depends on values chosen from a strategy. You could do this by putting an instance variable in the state machine that indicates whether the system under test has been initialized or not, and then using preconditions (below) to ensure that exactly one of the rules that initialize it get run before any rules that depend on it being initialized. Preconditions ------------- While it's possible to use :func:`~hypothesis.assume` in RuleBasedStateMachine rules, if you use it in only a few rules you can quickly run into a situation where few or none of your rules pass their assumptions. Thus, Hypothesis provides a :func:`~hypothesis.stateful.precondition` decorator to avoid this problem. The :func:`~hypothesis.stateful.precondition` decorator is used on ``rule``-decorated functions, and must be given a function that returns True or False based on the RuleBasedStateMachine instance. .. code:: python from hypothesis.stateful import RuleBasedStateMachine, precondition, rule class NumberModifier(RuleBasedStateMachine): num = 0 @rule() def add_one(self): self.num += 1 @precondition(lambda self: self.num != 0) @rule() def divide_with_one(self): self.num = 1 / self.num By using :func:`~hypothesis.stateful.precondition` here instead of :func:`~hypothesis.assume`, Hypothesis can filter the inapplicable rules before running them. This makes it much more likely that a useful sequence of steps will be generated. Note that currently preconditions can't access bundles; if you need to use preconditions, you should store relevant data on the instance instead. Invariants ---------- Often there are invariants that you want to ensure are met after every step in a process. It would be possible to add these as rules that are run, but they would be run zero or multiple times between other rules. Hypothesis provides a decorator that marks a function to be run after every step. .. code:: python from hypothesis.stateful import RuleBasedStateMachine, invariant, rule class NumberModifier(RuleBasedStateMachine): num = 0 @rule() def add_two(self): self.num += 2 if self.num > 50: self.num += 1 @invariant() def is_even(self): assert self.num % 2 == 0 NumberTest = NumberModifier.TestCase Invariants can also have :func:`~hypothesis.stateful.precondition`\ s applied to them, in which case they will only be run if the precondition function returns true. Note that currently invariants can't access bundles; if you need to use invariants, you should store relevant data on the instance instead. More fine grained control ------------------------- If you want to bypass the TestCase infrastructure you can invoke these manually. The stateful module exposes the function ``run_state_machine_as_test``, which takes an arbitrary function returning a RuleBasedStateMachine and an optional settings parameter and does the same as the class based runTest provided. ================================================ FILE: hypothesis-python/docs/tutorial/adapting-strategies.rst ================================================ Adapting strategies =================== This page discusses ways to adapt strategies to your needs, either by transforming them inline with |.map|, or filtering out unwanted inputs with |.filter| and |assume|. Mapping strategy inputs ----------------------- Sometimes you want to apply a simple transformation to a strategy. For instance, we know that we can generate lists of integers with ``lists(integers())``. But maybe we wanted to instead generate sorted lists. We could use an inline |.map| to achieve this: .. code-block:: pycon >>> lists(integers()).map(sorted).example() [-25527, -24245, -93, -70, -7, 0, 39, 65, 112, 6189, 19469, 32526, 1566924430] In general, ``strategy.map(f)`` returns a new strategy which transforms all the examples generated by ``strategy`` by calling ``f`` on them. Filtering strategy inputs ------------------------- Many strategies in Hypothesis offer some control over the kinds of values that get generated. For instance, ``integers(min_value=0)`` generates positive integers, and ``integers(100, 200)`` generates integers between ``100`` and ``200``. Sometimes, you need more control than this. The inputs from a strategy may not match exactly what you need, and you just need to filter out a few bad cases. For instance, suppose we have written a simple test involving the modulo operator ``%``: .. code-block:: python from hypothesis import given, strategies as st @given(st.integers(), st.integers()) def test_remainder_magnitude(a, b): # the remainder after division is always less than # the divisor assert abs(a % b) < abs(b) Hypothesis will quickly report a failure for this test: ``ZeroDivisionError: integer modulo by zero``. Just like division, modulo isn't defined for 0. The case of ``b == 0`` isn't interesting for the test, and we would like to get rid of it. The best way to do this is with the |.filter| method: .. code-block:: python from hypothesis import assume, given, strategies as st @given(st.integers(), st.integers().filter(lambda n: n != 0)) def test_remainder_magnitude(a, b): # b is guaranteed to be nonzero here, thanks to the filter assert abs(a % b) < abs(b) This test now passes cleanly. Calling |.filter| on a strategy creates a new strategy with that filter applied at generation-time. For instance, ``integers().filter(lambda n: n != 0)`` is a strategy which generates nonzero integers. Assuming away test cases ------------------------ |.filter| lets you filter test inputs from a single strategy. Hypothesis also provides an |assume| function for when you need to filter an entire test case, based on an arbitrary condition. The |assume| function skips test cases where some condition evaluates to ``True``. You can use it anywhere in your test. We could have expressed our |.filter| example above using |assume| as well: .. code-block:: python from hypothesis import assume, given, strategies as st @given(st.integers(), st.integers()) def test_remainder_magnitude(a, b): assume(b != 0) # b will be nonzero here assert abs(a % b) < abs(b) |assume| vs |.filter| ~~~~~~~~~~~~~~~~~~~~~ Where possible, you should use |.filter|. Hypothesis can often rewrite simple filters into more efficient sampling methods than rejection sampling, and will retry filters several times instead of aborting the entire test case (as with |assume|). For more complex relationships that can't be expressed with |.filter|, use |assume|. Here's an example of a test where we want to filter out two different types of examples: .. code-block:: python from hypothesis import assume, given, strategies as st @given(st.integers(), st.integers()) def test_floor_division_lossless_when_b_divides_a(a, b): # we want to assume that: # * b is nonzero, and # * b divides a assert (a // b) * b == a We could start by using |assume| for both: .. code-block:: python from hypothesis import assume, given, strategies as st @given(st.integers(), st.integers()) def test_floor_division_lossless_when_b_divides_a(a, b): assume(b != 0) assume(a % b == 0) assert (a // b) * b == a And then notice that the ``b != 0`` condition can be moved into the strategy definition as a |.filter| call: .. code-block:: python from hypothesis import assume, given, strategies as st @given(st.integers(), st.integers().filter(lambda n: n != 0)) def test_floor_division_lossless_when_b_divides_a(a, b): assume(a % b == 0) assert (a // b) * b == a However, the ``a % b == 0`` condition has to stay as an |assume|, because it expresses a more complicated relationship between ``a`` and ``b``. |assume| vs early-returning ~~~~~~~~~~~~~~~~~~~~~~~~~~~ One other way we could have avoided the divide-by-zero error inside the test function is to early-return when ``b == 0``: .. code-block:: python from hypothesis import assume, given, strategies as st @given(st.integers(), st.integers()) def test_remainder_magnitude(a, b): if b == 0: # bad plan - test "passes" without checking anything! return assert abs(a % b) < abs(b) While this would have avoided the divide-by-zero, early-returning is not the same as using |assume|. With |assume|, Hypothesis knows that a test case has been filtered out, and will not count it towards the |max_examples| limit. In contrast, early-returns are counted as a passing test, even though the assertions didn't run! In more complicted cases, this could end up testing your code less than you expect, because many test cases get discarded without Hypothesis knowing about it. In addition, |assume| lets you skip the test case at any point in the test, even inside arbitrarily deep nestings of functions. ================================================ FILE: hypothesis-python/docs/tutorial/adding-notes.rst ================================================ Adding notes ============ When a test fails, Hypothesis will normally print output that looks like this: .. code:: Falsifying example: test_a_thing(x=1, y="foo") Sometimes you want to add some additional information to a failure, such as the output of some intermediate step in your test. The |note| function lets you do this: .. code-block:: pycon >>> from hypothesis import given, note, strategies as st >>> @given(st.lists(st.integers()), st.randoms()) ... def test_shuffle_is_noop(ls, r): ... ls2 = list(ls) ... r.shuffle(ls2) ... note(f"Shuffle: {ls2!r}") ... assert ls == ls2 ... >>> try: ... test_shuffle_is_noop() ... except AssertionError: ... print("ls != ls2") ... Falsifying example: test_shuffle_is_noop(ls=[0, 1], r=RandomWithSeed(1)) Shuffle: [1, 0] ls != ls2 |note| is like a print statement that gets attached to the falsifying example reported by Hypothesis. It's also reported by :ref:`observability `, and shown for all examples (if |settings.verbosity| is set to |Verbosity.verbose| or higher). .. note:: |event| is a similar function which tells Hypothesis to count the number of test cases which reported each distinct value you pass, for inclusion in :ref:`test statistics ` and :ref:`observability reports `. ================================================ FILE: hypothesis-python/docs/tutorial/builtin-strategies.rst ================================================ Built-in strategies =================== This page shows some of the strategies that Hypothesis provides for you. Strategies provided by Hypothesis --------------------------------- Here is a selection of strategies provided by Hypothesis that may be useful to know: |st.integers| Generates integers. |st.floats| Generates floats. |st.booleans| Generates booleans. |st.text| Generates unicode strings (i.e., instances of |str|). Can be constrained to ASCII with ``st.text(st.characters(codec="ascii"))``. |st.lists| Generates lists with elements from the strategy passed to it. ``st.lists(st.integers())`` generates lists of integers. |st.tuples| Generates tuples of a fixed length. ``st.tuples(st.integers(), st.floats())`` generates tuples with two elements, where the first element is an integer and the second is a float. |st.one_of| Generates from any of the strategies passed to it. ``st.one_of(st.integers(), st.floats())`` generates either integers or floats. You can also use ``|`` to construct |st.one_of|, like ``st.integers() | st.floats()``. |st.builds| Generates instances of a class (or other callable) by specifying a strategy for each argument, like ``st.builds(Person, name=st.text(), age=st.integers())``. |st.just| Generates the exact value passed to it. ``st.just("a")`` generates the exact string ``"a"``. This is useful when something expects to be passed a strategy. For instance, ``st.lists(st.integers() | st.just("a"))`` generates lists whose elements are either integers or the string ``"a"``. |st.sampled_from| Generates a random value from a list. ``st.sampled_from(["a", 1])`` is roughly equivalent to ``st.just("a") | st.just(1)``. |st.none| Generates ``None``. Useful for parameters that can be optional, like ``st.integers() | st.none()``. .. seealso:: See the :doc:`strategies API reference ` for a full list of strategies provided by Hypothesis. ================================================ FILE: hypothesis-python/docs/tutorial/custom-strategies.rst ================================================ Custom strategies ================== This page describes how to write a custom strategy, for when the built-in strategies don't quite fit your needs. Writing helper functions ------------------------ Sometimes you might find it useful to write helper functions, to more concisely express a common pattern for your project. For example, it's much easier to write (and read!) ``response=json()`` than to have the whole implementation inline: .. code-block:: python def json(*, finite_only=True): """Helper function to describe JSON objects, with optional inf and nan.""" numbers = st.floats(allow_infinity=not finite_only, allow_nan=not finite_only) return st.recursive( st.none() | st.booleans() | st.integers() | numbers | st.text(), extend=lambda xs: st.lists(xs) | st.dictionaries(st.text(), xs), ) Writing your own strategy ------------------------- If a strategy in Hypothesis doesn't match what you need, you can write your own strategy. For instance, suppose we want to generate a list of floats which sum to ``1``. We might start implementing this by generating lists of integers between 0 and 1 with ``lists(floats(0, 1))``. But now we're a bit stuck, and can't go any further with the standard strategies. One way to define a new strategy is using the |st.composite| decorator. |st.composite| lets you define a new strategy that uses arbitrary Python code. For instance, to implement the above: .. code-block:: python from hypothesis import strategies as st @st.composite def sums_to_one(draw): l = draw(st.lists(st.floats(0, 1))) return [f / sum(l) for f in l] |st.composite| passes a ``draw`` function to the decorated function as its first argument. ``draw`` is used to draw a random value from another strategy. We return from ``sums_to_one`` a value of the form we wanted to generate; in this case, a list that sums to one. Let's see this new strategy in action: .. code-block:: python import pytest from hypothesis import given, strategies as st @st.composite def sums_to_one(draw): lst = draw(st.lists(st.floats(0.001, 1), min_size=1)) return [f / sum(lst) for f in lst] @given(sums_to_one()) def test(lst): # ignore floating point errors assert sum(lst) == pytest.approx(1) .. note:: Just like all other strategies, we called ``sums_to_one`` before passing it to |@given|. |st.composite| should be thought of as turning its decorated function into a function which returns a strategy when called. This is actually the same as existing strategies in Hypothesis; |st.integers| is really a function, which returns a strategy for integers when called. Combining |st.composite| with parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can add parameters to functions decorated with |st.composite|, including keyword-only arguments. These work as you would normally expect. For instance, suppose we wanted to generalize our ``sums_to_one`` function to ``sums_to_n``. We can add a parameter ``n``: .. code-block:: python import pytest from hypothesis import assume, given, strategies as st @st.composite def sums_to_n(draw, n=1): # <-- changed lst = draw(st.lists(st.floats(0, 1), min_size=1)) assume(sum(lst) > 0) return [f / sum(lst) * n for f in lst] # <-- changed @given(sums_to_n(10)) def test(lst): assert sum(lst) == pytest.approx(10) And we could just as easily have made ``n`` a keyword-only argument instead: .. code-block:: python import pytest from hypothesis import assume, given, strategies as st @st.composite def sums_to_n(draw, *, n=1): # <-- changed lst = draw(st.lists(st.floats(0, 1), min_size=1)) assume(sum(lst) > 0) return [f / sum(lst) * n for f in lst] @given(sums_to_n(n=10)) # <-- changed def test(lst): assert sum(lst) == pytest.approx(10) Dependent generation with |st.composite| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Another scenario where |st.composite| is useful is when generating a value that depends on a value from another strategy. For instance, suppose we wanted to generate two integers ``n1`` and ``n2`` with ``n1 <= n2``. We can do this using |st.composite|: .. code-block:: python @st.composite def ordered_pairs(draw): n1 = draw(st.integers()) n2 = draw(st.integers(min_value=n1)) return (n1, n2) @given(ordered_pairs()) def test_pairs_are_ordered(pair): n1, n2 = pair assert n1 <= n2 .. note:: We could also have written this particular strategy as ``st.tuples(st.integers(), st.integers()).map(sorted)`` (see :doc:`/tutorial/adapting-strategies`). Some prefer this inline approach, while others prefer defining well-named helper functions with |st.composite|. Our suggestion is simply that you prioritize ease of understanding when choosing which to use. Mixing data generation and test code ------------------------------------ When using |st.composite|, you have to finish generating the entire input before running your test. But maybe you don't want to generate all of the input until you're sure some initial test assertions have passed. Or maybe you have some complicated control flow which makes it necessary to generate something in the middle of the test. |st.data| lets you to do this. It's similar to |st.composite|, except it lets you mix test code and generation code. .. note:: The downside of this power is that |st.data| is incompatible |@example|, and that Hypothesis cannot print a nice representation of values generated from |st.data| when reporting failing examples, because the draws are spread out. Where possible, prefer |st.composite| to |st.data|. For instance, here's how we would write our earlier |st.composite| example using |st.data|: .. code-block:: python import pytest from hypothesis import given, strategies as st @given(st.data()) def test(data): lst = data.draw(st.lists(st.floats(0.001, 1), min_size=1)) lst = [f / sum(lst) for f in lst] # ignore floating point errors assert sum(lst) == pytest.approx(1) ================================================ FILE: hypothesis-python/docs/tutorial/flaky.rst ================================================ Flaky failures ============== Have you ever had a test fail, and then you re-run it, only for the test to magically pass? That is a *flaky test*. A flaky test is one which might behave differently when called again. You can think of it as a test which is not deterministic. Any test can be flaky, but because Hypothesis runs your test many times, Hypothesis tests are particularly likely to uncover flaky behavior. Note that Hypothesis does not require tests to be fully deterministic. Only the sequence of calls to Hypothesis APIs like ``draw`` from |st.composite| and the outcome of the test (pass or fail) need to be deterministic. This means you can use randomness, threads, or nondeterminism in your test, as long as it doesn't impact anything Hypothesis can see. Why is flakiness bad? --------------------- Hypothesis raises an exception when it detects flakiness. This might seem extreme, relative to a simple warning. But there are good reasons to consider flakiness a fatal error. .. TODO_DOCS: link to not-yet-written database page * Hypothesis relies on deterministic behavior for the database to work. * Flakiness makes debugging failures substantially harder if the failing input reported by Hypothesis only flakily reproduces. * Flakiness makes effectively exploration of the test's behavior space by Hypothesis difficult or impossible. Common sources of flakiness --------------------------- Here is a quick and non-exhaustive enumeration of some reasons you might encounter flakiness: * Decisions based on global state. * Explicit dependencies between inputs. * Test depends on filesystem or database state which isn't reset between inputs. * Un-managed sources of randomness. This includes standard PRNGs (see also |register_random|), but also thread scheduling, network timing, etc. .. note:: If your tests depend on global state, consider replacing that state with |st.shared|. This is a common way to refactor your test to bring conceptually-global state under the control and visibility of Hypothesis. Types of flakiness ------------------ When Hypothesis detects that a test is flaky, it will raise one of two |Flaky| exceptions. Flaky failure ~~~~~~~~~~~~~ The most common form of flakiness is that Hypothesis finds a failure, but then replaying that input does not reproduce the failure. For example, here is a contrived test which only fails the first time it is called: .. code-block:: python called = False @given(st.integers()) def test_fails_flakily(n): global called if not called: called = True assert False The first time Hypothesis generates an input, this test will fail. But when Hypothesis tries replaying that failure—by generating the same input—the test will succeed. This test is flaky. As a result, running ``test_fails_flakily()`` will raise |FlakyFailure|. |FlakyFailure| is an ``ExceptionGroup``, which contains the origin failure as a sub-exception: .. code-block:: none + Exception Group Traceback (most recent call last): | hypothesis.errors.FlakyFailure: Hypothesis test_fails_flakily(n=0) produces unreliable results: Falsified on the first call but did not on a subsequent one (1 sub-exception) | Falsifying example: test_fails_flakily( | n=0, | ) | Failed to reproduce exception. Expected: | Traceback (most recent call last): | File "/Users/tybug/Desktop/sandbox2.py", line 13, in test_fails_flakily | assert False | ^^^^^ | AssertionError +-+---------------- 1 ---------------- | ... | Traceback (most recent call last): | File "/Users/tybug/Desktop/sandbox2.py", line 13, in test_fails_flakily | assert False | ^^^^^ | AssertionError +------------------------------------ The solution to seeing |FlakyFailure| is to refactor the test to not depend on external state. In this case, the external state is the variable ``called``. Flaky strategy definition ~~~~~~~~~~~~~~~~~~~~~~~~~ Each strategy must 'do the same thing' (again, as seen by Hypothesis) if we replay a previously-seen input. Failing to do so is a more subtle but equally serious form of flakiness, which leaves us unable to shrink to a minimal failing input, or even reliably report the failure in future runs. One easy way for this to occur is if a strategy depends on external state. For example, this strategy filters out previously-generated integers, including those seen in any previous test case: .. code-block:: python seen = set() @st.composite def unique_ints(draw): while (n := draw(st.integers())) in seen: pass seen.add(n) return n @given(unique_ints(), unique_ints()) def test_ints(x, y): ... By using ``seen``, this test is relying on outside state! In the first test case where |st.integers| generates ``0``, ``unique_ints`` draws only one integer. But if in the next test case |st.integers| generates ``0``, ``unique_ints`` has to draw two integers because ``0`` is already in ``seen``. This means data generation is not deterministic. As a result, running ``test_ints()`` will raise |FlakyStrategyDefinition|. The solution is to refactor the strategy to not depend on external state. One way to do this is using |st.shared|: .. code-block:: python @st.composite def unique_ints(draw): seen_this_test = draw(st.shared(st.builds(set), key="seen_ints")) while (n := draw(st.integers())) in seen_this_test: pass seen_this_test.add(n) return n ================================================ FILE: hypothesis-python/docs/tutorial/index.rst ================================================ Tutorial ======== The Hypothesis tutorial introduces the main features of Hypothesis. We suggest reading through in order until completing :doc:`/tutorial/custom-strategies`, at which point you can choose to read what seems interesting to you. If you're in a hurry, the :doc:`quickstart ` is a much faster bare-bones version. .. toctree:: :maxdepth: 1 introduction builtin-strategies adapting-strategies custom-strategies settings replaying-failures adding-notes flaky ================================================ FILE: hypothesis-python/docs/tutorial/introduction.rst ================================================ Introduction to Hypothesis ========================== This page introduces two fundamental parts of Hypothesis (|@given|, and strategies) and shows how to test a selection sort implementation using Hypothesis. Install Hypothesis ------------------ First, let's install Hypothesis: .. code-block:: shell pip install hypothesis Defining a simple test ---------------------- Hypothesis tests are defined using two things; |@given|, and a *strategy*, which is passed to |@given|. Here's a simple example: .. code-block:: python from hypothesis import given, strategies as st @given(st.integers()) def test_is_integer(n): assert isinstance(n, int) Adding the |@given| decorator turns this function into a Hypothesis test. Passing |st.integers| to |@given| says that Hypothesis should generate random integers for the argument ``n`` when testing. We can run this test by calling it: .. code-block:: python from hypothesis import given, strategies as st @given(st.integers()) def test_is_integer(n): print(f"called with {n}") assert isinstance(n, int) test_is_integer() Note that we don't pass anything for ``n``; Hypothesis handles generating that value for us. The resulting output looks like this: .. code-block:: none called with 0 called with -18588 called with -672780074 called with 32616 ... Testing a sorting algorithm --------------------------- Suppose we have implemented a simple selection sort: .. code-block:: python # contents of example.py from hypothesis import given, strategies as st def selection_sort(lst): result = [] while lst: smallest = min(lst) result.append(smallest) lst.remove(smallest) return result and want to make sure it's correct. We can write the following test by combining the |st.integers| and |st.lists| strategies: .. code-block:: python ... @given(st.lists(st.integers())) def test_sort_correct(lst): print(f"called with {lst}") assert selection_sort(lst.copy()) == sorted(lst) test_sort_correct() When running ``test_sort_correct``, Hypothesis uses the ``lists(integers())`` strategy to generate random lists of integers. Feel free to run ``python example.py`` to get an idea of the kinds of lists Hypothesis generates (and to convince yourself that this test passes). Adding floats to our test ~~~~~~~~~~~~~~~~~~~~~~~~~ This test is a good start. But ``selection_sort`` should be able to sort lists with floats, too. If we wanted to generate lists of either integers or floats, we can change our strategy: .. code-block:: python # changes to example.py @given(st.lists(st.integers() | st.floats())) def test_sort_correct(lst): pass The pipe operator ``|`` takes two strategies, and returns a new strategy which generates values from either of its strategies. So the strategy ``integers() | floats()`` can generate either an integer, or a float. .. note:: ``strategy1 | strategy2`` is equivalent to :func:`st.one_of(strategy1, strategy2) `. Preventing |st.floats| from generating ``nan`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Even though ``test_sort_correct`` passed when we used lists of integers, it actually fails now that we've added floats! If you run ``python example.py``, you'll likely (but not always; this is random testing, after all) find that Hypothesis reports a counterexample to ``test_sort_correct``. For me, that counterexample is ``[1.0, nan, 0]``. It might be different for you. The issue is that sorting in the presence of ``nan`` is not well defined. As a result, we may decide that we don't want to generate them while testing. We can pass ``floats(allow_nan=False)`` to tell Hypothesis not to generate ``nan``: .. code-block:: python # changes to example.py @given(st.lists(st.integers() | st.floats(allow_nan=False))) def test_sort_correct(lst): pass And now this test passes without issues. .. note:: You can use the |.example()| method to get an idea of the kinds of things a strategy will generate: .. code-block:: pycon >>> st.lists(st.integers() | st.floats(allow_nan=False)).example() [-5.969063e-08, 15283673678, 18717, -inf] Note that |.example()| is intended for interactive use only (i.e., in a :term:`REPL `). It is not intended to be used inside tests. Tests with multiple arguments ----------------------------- If we wanted to pass multiple arguments to a test, we can do this by passing multiple strategies to |@given|: .. code-block:: python from hypothesis import given, strategies as st @given(st.integers(), st.lists(st.floats())) def test_multiple_arguments(n, lst): assert isinstance(n, int) assert isinstance(lst, list) for f in lst: assert isinstance(f, float) Keyword arguments ~~~~~~~~~~~~~~~~~ We can also pass strategies using keyword arguments: .. code-block:: python @given(lst=st.lists(st.floats()), n=st.integers()) # <-- changed def test_multiple_arguments(n, lst): pass Note that even though we changed the order the parameters to |@given| appear, we also explicitly told it which parameters to pass to by using keyword arguments, so the meaning of the test hasn't changed. In general, you can think of positional and keyword arguments to |@given| as being forwarded to the test arguments. .. note:: One exception is that |@given| does not support mixing positional and keyword arguments. See the |@given| documentation for more about how it handles arguments. Running Hypothesis tests ------------------------ There are a few ways to run a Hypothesis test. * Explicitly call it, like ``test_is_integer()``, as we've seen. Hypothesis tests are just normal functions, except |@given| handles generating and passing values for the function arguments. * Let a test runner such as :pypi:`pytest` pick up on it (as long as the function name starts with ``test_``). Concretely, when running a Hypothesis test, Hypothesis will: * generate 100 random inputs, * run the body of the function for each input, and * report any exceptions that get raised. .. note:: The number of examples can be controlled with the |max_examples| setting. The default is 100. When to use Hypothesis and property-based testing ------------------------------------------------- Property-based testing is a powerful *addition* to unit testing. It is not always a replacement. If you're having trouble coming up with a property in your code to test, we recommend trying the following: * Look for round-trip properties: encode/decode, serialize/deserialize, etc. These property-based tests tend to be both powerful and easy to write. * Look for ``@pytest.mark.parametrize`` in your existing tests. This is sometimes a good hint you can replace the parametrization with a strategy. For instance, ``@pytest.mark.parametrize("n", range(0, 100))`` could be replaced by ``@given(st.integers(0, 100 - 1))``. * Simply call your code with random inputs (of the correct shape) from Hypothesis! You might be surprised how often this finds crashes. This can be especially valuable for projects with a single entrypoint interface to a lot of underlying code. Other examples of properties include: * An optimized implementation is equivalent to a slower, but clearly correct, implementation. * A sequence of transactions in a financial system always "balances"; money never gets lost. * The derivative of a polynomial of order ``n`` has order ``n - 1``. * A type-checker, linter, formatter, or compiler does not crash when called on syntactically valid code. * `And more `_. ================================================ FILE: hypothesis-python/docs/tutorial/replaying-failures.rst ================================================ Replaying failed tests ====================== Replaying failures found by your Hypothesis tests is almost as important as finding failures in the first place. Hypothesis therefore contains several ways to replay failures: they are automatically saved to (and replayed from) a local |ExampleDatabase|, and can be manually replayed via |@example| or |@reproduce_failure|. The Hypothesis database ----------------------- When a test fails, Hypothesis automatically saves the failure so it can be replayed later. For instance, the first time you run the following code, it will take up to a few seconds to fail: .. code-block:: python import time from hypothesis import strategies as st @given(st.integers()) def f(n): assert n < 50 time.sleep(0.1) f() But the next time you run this code, it will fail instantly. When Hypothesis saw the failure the first time, it automatically saved that failing input. On future runs, Hypothesis retries any failing inputs (in |Phase.explain|) before generating new inputs (in |Phase.generate|) Hypothesis saves failures to the |settings.database| setting. By default, this is a |DirectoryBasedExampleDatabase| in the local ``.hypothesis`` directory. Disabling the database ~~~~~~~~~~~~~~~~~~~~~~ You can disable the database by passing ``database=None`` to |@settings|: .. code-block:: python @settings(database=None) def f(n): ... Using |@example| to run a specific input ---------------------------------------- If you want Hypothesis to always run a specific input, you can use |@example|. |@example| adds an explicit input which Hypothesis will run every time, in addition to the randomly generated examples. You can think of |@example| as combining unit-testing with property-based testing. For instance, suppose we write a test using |st.integers|, but want to make sure we try a few special prime numbers every time we run the test. We can add these inputs with |@example|: .. code-block:: python # two mersenne primes @example(2**17 - 1) @example(2**19 - 1) @given(st.integers()) def test_integers(n): pass test_integers() Hypothesis runs all explicit examples first, in the |Phase.explicit| phase, before generating additional random examples in the |Phase.generate| phase. Inputs from |@example| do not shrink ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Note that unlike examples generated by Hypothesis, examples provided using |@example| do not shrink. We can see this by adding a failing assertion: .. code-block:: python @example(2**17 - 1) @given(st.integers()) def test_something_with_integers(n): assert n < 100 Hypothesis will print ``Falsifying explicit example: test_something_with_integers(n=131071)``, instead of shrinking to ``n=100``. Prefer |@example| over the database for correctness --------------------------------------------------- .. TODO_DOCS: link to /explanation/database-keys While the database is useful for quick local iteration, Hypothesis may invalidate it when upgrading (because e.g. the internal format may have changed). Changes to the source code of a test function may also change its database key, invalidating its stored entries. We therefore recommend against relying on the database for the correctness of your tests. If you want to ensure an input is run every time, use |@example|. Replaying examples with |@reproduce_failure| -------------------------------------------- If |settings.print_blob| is set to ``True`` (the default in the ``ci`` settings profile), and a test fails, Hypothesis will print an |@reproduce_failure| decorator containing an opaque blob as part of the error message: .. code-block:: pycon >>> from hypothesis import settings, given >>> import hypothesis.strategies as st >>> @given(st.floats()) ... @settings(print_blob=True) ... def test(f): ... assert f == f ... >>> test() ... Falsifying example: test( f=nan, ) You can reproduce this example by temporarily adding @reproduce_failure('6.131.23', b'ACh/+AAAAAAAAA==') as a decorator on your test case You can add this decorator to your test to reproduce the failure. This can be useful for locally replaying failures found by CI. Note that the binary blob is not stable across Hypothesis versions, so you should not leave this decorator on your tests permanently. Use |@example| with an explicit input instead. Sharing failures with the database ---------------------------------- If you work with multiple developers, or want to share failures across environments (such as locally replaying a failure found by CI), another option is to share the Hypothesis database. For instance, by setting |settings.database| to an instance of a networked database like |RedisExampleDatabase|, any developer connecting to that networked database will automatically replay any failures found by other developers. Similarly, setting |settings.database| to |GitHubArtifactDatabase| will automatically replay any failures found by the connected CI artifact. ================================================ FILE: hypothesis-python/docs/tutorial/settings.rst ================================================ Configuring test settings ========================= This page discusses how to configure the behavior of a single Hypothesis test, or of an entire test suite. Configuring a single test ------------------------- Hypothesis lets you configure the default behavior of a test using the |@settings| decorator. You can use settings to configure how many examples Hypothesis generates, how Hypothesis replays failing examples, and the verbosity level of the test, among others. Using |@settings| on a single test looks like this: .. code-block:: python from hypothesis import given, settings, strategies as st @given(st.integers()) @settings(max_examples=200) def runs_200_times_instead_of_100(n): pass You can put |@settings| either before or after |@given|. Both are equivalent. Changing the number of examples ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you have a test which is very expensive or very cheap to run, you can change the number of examples (inputs) Hypothesis generates with the |max_examples| setting: .. code-block:: python from hypothesis import given, settings, strategies as st @given(st.integers()) @settings(max_examples=5) def test(n): print("prints five times") The default is 100 examples. .. note:: See :doc:`../explanation/example-count` for details on how |max_examples| interacts with other parts of Hypothesis. Other settings options ~~~~~~~~~~~~~~~~~~~~~~ Here are a few of the more commonly used setting values: * |settings.phases| controls which phases of Hypothesis run, like replaying from the database or generating new inputs. * |settings.database| controls how and if Hypothesis replays failing examples. * |settings.verbosity| can print debug information. * |settings.derandomize| makes Hypothesis deterministic. (`Two kinds of testing `__ discusses when and why you might want that). .. note:: See the |settings| reference for a full list of possible settings. Changing settings across your test suite ---------------------------------------- In addition to configuring individual test functions with |@settings|, you can configure test behavior across your test suite using a settings profile. This might be useful for creating a development settings profile which runs fewer examples, or a settings profile in CI which connects to a separate database. To create a settings profile, use |settings.register_profile|: .. code-block:: python from hypothesis import HealthCheck, settings settings.register_profile("fast", max_examples=10) You can place this code in any file which gets loaded before your tests get run. This includes an ``__init__.py`` file in the test directory or any of the test files themselves. If using pytest, the standard location to place this code is in a ``confest.py`` file (though an ``__init__.py`` or test file will also work). Note that registering a new profile will not affect tests until it is loaded with |settings.load_profile|: .. code-block:: python from hypothesis import HealthCheck, settings settings.register_profile("fast", max_examples=10) # any tests executed before loading this profile will still use the # default active profile of 100 examples. settings.load_profile("fast") # any tests executed after this point will use the active fast # profile of 10 examples. There is no limit to the number of settings profiles you can create. Hypothesis creates a profile called ``"default"``, which is active by default. You can also explicitly make it active at any time using ``settings.load_profile("default")``, if for instance you wanted to revert a custom profile you had previously loaded. Loading profiles from environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Using an environment variable to load a settings profile is a useful trick for choosing a settings profile depending on the environment: .. code-block:: pycon >>> import os >>> from hypothesis import settings, Verbosity >>> settings.register_profile("long", max_examples=1000) >>> settings.register_profile("fast", max_examples=10) >>> settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose) >>> settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) If using pytest, you can also easily select the active profile with ``--hypothesis-profile``: .. code:: bash $ pytest --hypothesis-profile fast See the :ref:`Hypothesis pytest plugin `. ================================================ FILE: hypothesis-python/docs/usage.rst ================================================ Projects using Hypothesis ========================= Hypothesis is downloaded `over 4 million times each week `__, and was used by `more than 5% of Python users surveyed by the PSF in 2023 `__! The following notable open-source projects use Hypothesis to test their code: `pytorch `_, `jax `_, `PyPy `_, `numpy `_, `pandas `_, `attrs `_, `chardet `_, `bidict `_, `xarray `_, `array-api-tests `_, `pandera `_, `ivy `_, `zenml `_, `mercurial `_, `qutebrowser `_, `dry-python/returns `_, `argon2_cffi `_, `axelrod `_, `hyper-h2 `_, `MDAnalysis `_, `napari `_, `natsort `_, `vdirsyncer `_, and `pyrsistent `_. You can find `thousands more projects tested by Hypothesis on GitHub `__. There are also dozens of :doc:`first-party ` and :doc:`third-party extensions ` integrating Hypothesis with a wide variety of libraries and data formats. .. ^ citations that I put effort into looking up but decided not to use. Maybe we'll use them in the future? .. https://github.com/pytorch/pytorch/blob/59ad8f1ac6bce11617a5f856df9e88b3bf9266af/pyproject.toml#L41 .. https://github.com/jax-ml/jax/blob/48335107f82117ac34c76cac3e22546d2da78eaf/build/test-requirements.txt#L5 .. https://github.com/pypy/pypy/blob/338295bd0567cda9a3c603f428b14229da08e750/extra_tests/requirements.txt#L2 .. https://github.com/numpy/numpy/blob/c9b2919556789675dca0e202dd5a4b46d7d23ff2/requirements/test_requirements.txt#L5 .. https://github.com/pandas-dev/pandas/blob/1863adb252863b718ba29912922bf050ce0eaa3d/pyproject.toml#L60 .. https://github.com/python-attrs/attrs/blob/5084de361bf9e722dda6876e6e2b8ce8c63b7272/pyproject.toml#L47 .. https://github.com/chardet/chardet/blob/8e8dfcd93c572c2cbe37585e01662a90b16fbab6/pyproject.toml#L59 .. https://github.com/jab/bidict/blob/0116e5b772bd2e390267c511187e60931b733153/pyproject.toml#L38 .. https://github.com/pydata/xarray/blob/3572f4e70f2b12ef9935c1f8c3c1b74045d2a092/pyproject.toml#L73 .. https://foss.heptapod.net/mercurial/mercurial-devel/-/blob/b8ca286fda2eb275ffdfd7417fb539a03748d22c/tests/hypothesishelpers.py .. https://github.com/qutebrowser/qutebrowser/blob/642c5fe2fe46082de53219c19e02fef209753aa0/misc/requirements/requirements-tests.txt#L19 Research papers about Hypothesis -------------------------------- Looking to read more about Hypothesis and property-based testing? Hypothesis has been the subject of a number of research papers: 1. `Hypothesis: A new approach to property-based testing `_ (2019)* 2. `Test-Case Reduction via Test-Case Generation: Insights from the Hypothesis Reducer `_ (2020)* 3. `Deriving semantics-aware fuzzers from web API schemas `_ (2022)* 4. `Tyche: Making Sense of PBT Effectiveness `_ (2024)* 5. `An Empirical Evaluation of Property-Based Testing in Python `_ (2025) 6. `Agentic Property-Based Testing: Finding Bugs Across the Python Ecosystem `_ (2025)* \* *Author list includes one or more Hypothesis maintainers* ================================================ FILE: hypothesis-python/examples/README.md ================================================ # Examples of Hypothesis usage This is a directory for examples of using Hypothesis that show case its features or demonstrate a useful way of testing something. Right now it's a bit small and fairly algorithmically focused. Pull requests to add more examples would be *greatly* appreciated, especially ones using e.g. the Django integration or testing something "Businessy". ================================================ FILE: hypothesis-python/examples/example_hypothesis_entrypoint/example_hypothesis_entrypoint.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This example demonstrates a setuptools entry point. See https://hypothesis.readthedocs.io/en/latest/strategies.html#registering-strategies-via-setuptools-entry-points for details and documentation. """ class MyCustomType: def __init__(self, x: int): assert x >= 0, f"got {x}, but only positive numbers are allowed" self.x = x def _hypothesis_setup_hook(): import hypothesis.strategies as st st.register_type_strategy(MyCustomType, st.integers(min_value=0).map(MyCustomType)) ================================================ FILE: hypothesis-python/examples/example_hypothesis_entrypoint/setup.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Minimal setup.py to register an entrypoint.""" import setuptools setuptools.setup( name="example_hypothesis_entrypoint", author="Example Author", email="author@example.com", license="MPL v2", description="Minimal setup.py to register an entrypoint.", packages=setuptools.find_packages(), install_requires=["hypothesis"], python_requires=">=3.7", entry_points={ "hypothesis": ["_ = example_hypothesis_entrypoint:_hypothesis_setup_hook"] }, ) ================================================ FILE: hypothesis-python/examples/example_hypothesis_entrypoint/test_entrypoint.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from example_hypothesis_entrypoint import MyCustomType from hypothesis import given, strategies as st @given(st.from_type(MyCustomType)) def test_registered_from_entrypoint(x): # This demonstrates that we've registered the type, not just # worked out how to construct an instance. assert isinstance(x, MyCustomType) assert x.x >= 0 ================================================ FILE: hypothesis-python/examples/test_basic.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st # Our tested class class Product: def __init__(self, price: float) -> None: self.price: float = price def get_discount_price(self, discount_percentage: float): return self.price * (discount_percentage / 100) # The @given decorator generates examples for us! @given( price=st.floats(min_value=0, allow_nan=False, allow_infinity=False), discount_percentage=st.floats( min_value=0, max_value=100, allow_nan=False, allow_infinity=False ), ) def test_a_discounted_price_is_not_higher_than_the_original_price( price, discount_percentage ): product = Product(price) assert product.get_discount_price(discount_percentage) <= product.price ================================================ FILE: hypothesis-python/examples/test_binary_search.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This file demonstrates testing a binary search. It's a useful example because the result of the binary search is so clearly determined by the invariants it must satisfy, so we can simply test for those invariants. It also demonstrates the useful testing technique of testing how the answer should change (or not) in response to movements in the underlying data. """ import itertools from hypothesis import given, strategies as st def binary_search(ls, v): """Take a list ls and a value v such that ls is sorted and v is comparable with the elements of ls. Return an index i such that 0 <= i <= len(v) with the properties: 1. ls.insert(i, v) is sorted 2. ls.insert(j, v) is not sorted for j < i """ # Without this check we will get an index error on the next line when the # list is empty. if not ls: return 0 # Without this check we will miss the case where the insertion point should # be zero: The invariant we maintain in the next section is that lo is # always strictly lower than the insertion point. if v <= ls[0]: return 0 # Invariant: There is no insertion point i with i <= lo lo = 0 # Invariant: There is an insertion point i with i <= hi hi = len(ls) while lo + 1 < hi: mid = (lo + hi) // 2 if v > ls[mid]: # Inserting v anywhere below mid would result in an unsorted list # because it's > the value at mid. Therefore mid is a valid new lo lo = mid # Uncommenting the following lines will cause this to return a valid # insertion point which is not always minimal. # elif v == ls[mid]: # return mid else: # Either v == ls[mid] in which case mid is a valid insertion point # or v < ls[mid], in which case all valid insertion points must be # < hi. Either way, mid is a valid new hi. hi = mid assert lo + 1 == hi # We now know that there is a valid insertion point <= hi and there is no # valid insertion point < hi because hi - 1 is lo. Therefore hi is the # answer we were seeking return hi def is_sorted(ls): """Is this list sorted?""" return all(x <= y for x, y in itertools.pairwise(ls)) Values = st.integers() # We generate arbitrary lists and turn this into generating sorting lists # by just sorting them. SortedLists = st.lists(Values).map(sorted) # We could also do it this way, but that would be a bad idea: # SortedLists = st.lists(Values).filter(is_sorted) # The problem is that Hypothesis will only generate long sorted lists with very # low probability, so we are much better off post-processing values into the # form we want than filtering them out. @given(ls=SortedLists, v=Values) def test_insert_is_sorted(ls, v): """We test the first invariant: binary_search should return an index such that inserting the value provided at that index would result in a sorted set.""" ls.insert(binary_search(ls, v), v) assert is_sorted(ls) @given(ls=SortedLists, v=Values) def test_is_minimal(ls, v): """We test the second invariant: binary_search should return an index such that no smaller index is a valid insertion point for v.""" for i in range(binary_search(ls, v)): ls2 = list(ls) ls2.insert(i, v) assert not is_sorted(ls2) @given(ls=SortedLists, v=Values) def test_inserts_into_same_place_twice(ls, v): """In this we test a *consequence* of the second invariant: When we insert a value into a list twice, the insertion point should be the same both times. This is because we know that v is > the previous element and == the next element. In theory if the former passes, this should always pass. In practice, failures are detected by this test with much higher probability because it deliberately puts the data into a shape that is likely to trigger a failure. This is an instance of a good general category of test: Testing how the function moves in responses to changes in the underlying data. """ i = binary_search(ls, v) ls.insert(i, v) assert binary_search(ls, v) == i ================================================ FILE: hypothesis-python/examples/test_rle.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This example demonstrates testing a run length encoding scheme. That is, we take a sequence and represent it by a shorter sequence where each 'run' of consecutive equal elements is represented as a single element plus a count. So e.g. [1, 1, 1, 1, 2, 1] is represented as [[1, 4], [2, 1], [1, 1]] This demonstrates the useful decode(encode(x)) == x invariant that is often a fruitful source of testing with Hypothesis. It also has an example of testing invariants in response to changes in the underlying data. """ from hypothesis import assume, given, strategies as st def run_length_encode(seq): """Encode a sequence as a new run-length encoded sequence.""" if not seq: return [] # By starting off the count at zero we simplify the iteration logic # slightly. result = [[seq[0], 0]] for s in seq: if ( # If you uncomment this line this branch will be skipped and we'll # always append a new run of length 1. Note which tests fail. # False and s == result[-1][0] # Try uncommenting this line and see what problems occur: # and result[-1][-1] < 2 ): result[-1][1] += 1 else: result.append([s, 1]) return result def run_length_decode(seq): """Take a previously encoded sequence and reconstruct the original from it.""" result = [] for s, i in seq: for _ in range(i): result.append(s) return result # We use lists of a type that should have a relatively high duplication rate, # otherwise we'd almost never get any runs. Lists = st.lists(st.integers(0, 10)) @given(Lists) def test_decodes_to_starting_sequence(ls): """If we encode a sequence and then decode the result, we should get the original sequence back. Otherwise we've done something very wrong. """ assert run_length_decode(run_length_encode(ls)) == ls @given(Lists, st.data()) def test_duplicating_an_element_does_not_increase_length(ls, data): """The previous test could be passed by simply returning the input sequence so we need something that tests the compression property of our encoding. In this test we deliberately introduce or extend a run and assert that this does not increase the length of our encoding, because they should be part of the same run in the final result. """ # We use assume to get a valid index into the list. We could also have used # e.g. flatmap, but this is relatively straightforward and will tend to # perform better. assume(ls) i = data.draw(st.integers(0, len(ls) - 1)) ls2 = list(ls) # duplicating the value at i right next to it guarantees they are part of # the same run in the resulting compression. ls2.insert(i, ls2[i]) assert len(run_length_encode(ls2)) == len(run_length_encode(ls)) ================================================ FILE: hypothesis-python/pyproject.toml ================================================ [build-system] # require a recent setuptools for `license = ` support requires = ["setuptools >= 78.1.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "hypothesis" # see [tool.setuptools.dynamic] dynamic = ["version"] authors = [ { name = "David R. MacIver and Zac Hatfield-Dodds", email = "david@drmaciver.com" } ] description = "The property-based testing library for Python" license = "MPL-2.0" requires-python = ">= 3.10" keywords = ["python", "testing", "fuzzing", "property-based-testing"] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Hypothesis", "Framework :: Pytest", "Intended Audience :: Developers", "Operating System :: Unix", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Education :: Testing", "Topic :: Software Development :: Testing", "Typing :: Typed", ] dependencies = [ "exceptiongroup>=1.0.0; python_version<'3.11'", "sortedcontainers>=2.1.0,<3.0.0", ] # Avoid changing this by hand. This is automatically updated by update_changelog_and_version. [project.readme] text = """
# Hypothesis * [Website](https://hypothesis.works/) * [Documentation](https://hypothesis.readthedocs.io/en/latest/) * [Source code](https://github.com/hypothesisWorks/hypothesis/) * [Contributing](https://github.com/HypothesisWorks/hypothesis/blob/master/CONTRIBUTING.rst) * [Community](https://hypothesis.readthedocs.io/en/latest/community.html) Hypothesis is the property-based testing library for Python. With Hypothesis, you write tests which should pass for all inputs in whatever range you describe, and let Hypothesis randomly choose which of those inputs to check - including edge cases you might not have thought about. For example: ```python from hypothesis import given, strategies as st @given(st.lists(st.integers())) def test_matches_builtin(ls): assert sorted(ls) == my_sort(ls) ``` This randomized testing can catch bugs and edge cases that you didn't think of and wouldn't have found. In addition, when Hypothesis does find a bug, it doesn't just report any failing example — it reports the simplest possible one. This makes property-based tests a powerful tool for debugging, as well as testing. For instance, ```python def my_sort(ls): return sorted(set(ls)) ``` fails with the simplest possible failing example: ``` Falsifying example: test_matches_builtin(ls=[0, 0]) ``` ### Installation To install Hypothesis: ``` pip install hypothesis ``` There are also [optional extras available](https://hypothesis.readthedocs.io/en/latest/extras.html). """ content-type = "text/markdown" [project.urls] homepage = "https://hypothesis.works" source = "https://github.com/HypothesisWorks/hypothesis" changelog = "https://hypothesis.readthedocs.io/en/latest/changelog.html" documentation = "https://hypothesis.readthedocs.io" issues = "https://github.com/HypothesisWorks/hypothesis/issues" [project.optional-dependencies] cli = ["click>=7.0", "black>=20.8b0", "rich>=9.0.0"] codemods = ["libcst>=0.3.16"] ghostwriter = ["black>=20.8b0"] pytz = ["pytz>=2014.1"] dateutil = ["python-dateutil>=1.4"] lark = ["lark>=0.10.1"] # probably still works with old `lark-parser` too numpy = ["numpy>=1.21.6"] # oldest with wheels for non-EOL Python (for now) pandas = ["pandas>=1.1"] pytest = ["pytest>=4.6"] dpcontracts = ["dpcontracts>=0.4"] redis = ["redis>=3.0.0"] crosshair = ["hypothesis-crosshair>=0.0.27", "crosshair-tool>=0.0.102"] # zoneinfo is an odd one: every dependency is platform-conditional. zoneinfo = ["tzdata>=2025.3; sys_platform == 'win32' or sys_platform == 'emscripten'"] # We only support Django versions with upstream support - see # https://www.djangoproject.com/download/#supported-versions # We also leave the choice of timezone library to the user, since it # might be zoneinfo or pytz depending on version and configuration. django = ["django>=4.2"] watchdog = ["watchdog>=4.0.0"] # Avoid changing this by hand. This is automatically updated by update_changelog_and_version all = ["black>=20.8b0", "click>=7.0", "crosshair-tool>=0.0.102", "django>=4.2", "dpcontracts>=0.4", "hypothesis-crosshair>=0.0.27", "lark>=0.10.1", "libcst>=0.3.16", "numpy>=1.21.6", "pandas>=1.1", "pytest>=4.6", "python-dateutil>=1.4", "pytz>=2014.1", "redis>=3.0.0", "rich>=9.0.0", "tzdata>=2025.3; sys_platform == 'win32' or sys_platform == 'emscripten'", "watchdog>=4.0.0"] [tool.setuptools.dynamic] version = {attr = "hypothesis.version.__version__"} [tool.setuptools.package-data] hypothesis = ["vendor/tlds-alpha-by-domain.txt"] [project.scripts] hypothesis = "hypothesis.extra.cli:main" [project.entry-points.pytest11] hypothesispytest = "_hypothesis_pytestplugin" [tool.coverage.run] parallel = true branch = true source = ["hypothesis"] omit = [ "**/_hypothesis_ftz_detector.py", "**/_hypothesis_pytestplugin.py", "**/_hypothesis_globals.py", "**/extra/array_api.py", "**/extra/cli.py", "**/extra/django/*.py", "**/extra/ghostwriter.py", "**/extra/pytestplugin.py", "**/internal/scrutineer.py", "**/utils/terminal.py", "**/internal/conjecture/provider_conformance.py", # covered by our attrs tests in check-niche "**/strategies/_internal/attrs.py" ] [tool.coverage.report] fail_under = 100 show_missing = true skip_covered = true exclude_lines = [ "pragma: no cover", "raise NotImplementedError", "def __repr__", "def _repr_pretty_", "def __ne__", "def __copy__", "def __deepcopy__", "except ImportError:", "except ModuleNotFoundError:", "if PYPY:", "if TYPE_CHECKING:", "if sys\\.version_info", "if \"[\\w\\.]+\" in sys\\.modules:", "if .+ := sys\\.modules\\.get\\(\"[\\w\\.]+\"\\)", "@overload", "if .*\\bnot .*provider.avoid_realization", ] ================================================ FILE: hypothesis-python/pyrightconfig.json ================================================ { "include": ["src"], "typeCheckingMode": "strict" } ================================================ FILE: hypothesis-python/scripts/basic-test.sh ================================================ #!/bin/bash set -e -o xtrace HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$HERE/.." python -c ' import os for k, v in sorted(dict(os.environ).items()): print("%s=%s" % (k, v)) ' pip install . PYTEST="python -bb -X dev -m pytest -nauto --durations-min=1.0" # Run all the no-extra-dependency tests for this version (except slow nocover tests) $PYTEST tests/cover tests/pytest # Run tests for each extra module while the requirements are installed pip install ".[pytz, dateutil, zoneinfo]" $PYTEST tests/datetime/ pip uninstall -y pytz python-dateutil pip install ".[dpcontracts]" $PYTEST tests/dpcontracts/ pip uninstall -y dpcontracts # use pinned redis version instead of inheriting from fakeredis pip install "$(grep '^redis==' ../requirements/coverage.txt)" pip install "$(grep 'fakeredis==' ../requirements/coverage.txt)" $PYTEST tests/redis/ pip uninstall -y redis fakeredis pip install "$(grep 'typing-extensions==' ../requirements/coverage.txt)" $PYTEST tests/typing_extensions/ pip uninstall -y typing_extensions pip install ".[lark]" pip install "$(grep -m 1 -oE 'lark>=([0-9.]+)' ../hypothesis-python/pyproject.toml | tr '>' =)" $PYTEST -Wignore tests/lark/ pip install "$(grep 'lark==' ../requirements/coverage.txt)" $PYTEST tests/lark/ pip uninstall -y lark if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel == \'final\' and platform.python_implementation() not in ("PyPy", "GraalVM"))')" = "True" ] ; then pip install ".[codemods,cli]" $PYTEST tests/codemods/ pip uninstall -y libcst click if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 10))')" = "True" ] ; then # Per NEP-29, this is the last version to support Python 3.10 pip install numpy==2.2.6 else pip install "$(grep 'numpy==' ../requirements/coverage.txt)" fi pip install "$(grep -E 'black(==| @)' ../requirements/coverage.txt)" $PYTEST tests/ghostwriter/ pip uninstall -y black numpy fi if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 10))')" = "False" ] ; then exit 0 fi $PYTEST tests/nocover/ # Run some tests without docstrings or assertions, to catch bugs # like issue #822 in one of the test decorators. See also #1541. PYTHONOPTIMIZE=2 $PYTEST \ -W'ignore:assertions not in test modules or plugins will be ignored because assert statements are not executed by the underlying Python interpreter:pytest.PytestConfigWarning' \ -W'ignore:Module already imported so cannot be rewritten:pytest.PytestAssertRewriteWarning' \ tests/cover/test_testdecorators.py case "$(python -c 'import platform; print(platform.python_implementation())')" in PyPy|GraalVM) ;; *) pip install .[django] HYPOTHESIS_DJANGO_USETZ=TRUE python -m tests.django.manage test tests.django HYPOTHESIS_DJANGO_USETZ=FALSE python -m tests.django.manage test tests.django pip uninstall -y django pytz pip install "$(grep 'numpy==' ../requirements/coverage.txt)" $PYTEST tests/array_api $PYTEST tests/numpy pip install "$(grep 'pandas==' ../requirements/coverage.txt)" $PYTEST tests/pandas esac ================================================ FILE: hypothesis-python/scripts/other-tests.sh ================================================ #!/bin/bash set -e -o xtrace HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$HERE/.." pip install . PYTEST="python -bb -X dev -m pytest -nauto --durations-min=1.0" # Run some tests without docstrings or assertions, to catch bugs # like issue #822 in one of the test decorators. See also #1541. PYTHONOPTIMIZE=2 $PYTEST \ -W'ignore:assertions not in test modules or plugins will be ignored because assert statements are not executed by the underlying Python interpreter:pytest.PytestConfigWarning' \ -W'ignore:Module already imported so cannot be rewritten:pytest.PytestAssertRewriteWarning' \ tests/cover/test_testdecorators.py # Run tests for each extra module while the requirements are installed pip install ".[pytz, dateutil, zoneinfo]" $PYTEST tests/datetime/ pip uninstall -y pytz python-dateutil pip install ".[dpcontracts]" $PYTEST tests/dpcontracts/ pip uninstall -y dpcontracts pip install attrs $PYTEST tests/attrs/ pip uninstall -y attrs # use pinned redis version instead of inheriting from fakeredis pip install "$(grep '^redis==' ../requirements/coverage.txt)" pip install "$(grep 'fakeredis==' ../requirements/coverage.txt)" pip install "$(grep 'typing-extensions==' ../requirements/coverage.txt)" $PYTEST tests/redis/ pip uninstall -y redis fakeredis $PYTEST tests/typing_extensions/ if [ "$HYPOTHESIS_PROFILE" != "crosshair" ] && [ "$(python -c 'import sys; print(sys.version_info[:2] > (3, 10))')" = "True" ]; then pip uninstall -y typing-extensions fi pip install "$(grep 'annotated-types==' ../requirements/coverage.txt)" $PYTEST tests/test_annotated_types.py pip uninstall -y annotated-types pip install ".[lark]" pip install "$(grep -m 1 -oE 'lark>=([0-9.]+)' ../hypothesis-python/pyproject.toml | tr '>' =)" $PYTEST -Wignore tests/lark/ pip install "$(grep 'lark==' ../requirements/coverage.txt)" $PYTEST tests/lark/ pip uninstall -y lark if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel == \'final\' and platform.python_implementation() not in ("PyPy", "GraalVM"))')" = "True" ] ; then pip install ".[codemods,cli]" $PYTEST tests/codemods/ if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 10))')" = "True" ] ; then # Per NEP-29, this is the last version to support Python 3.10 pip install numpy==2.2.6 else pip install "$(grep 'numpy==' ../requirements/coverage.txt)" fi pip install "$(grep -E 'black(==| @)' ../requirements/coverage.txt)" $PYTEST tests/patching/ pip uninstall -y libcst # One of the ghostwriter tests uses attrs (though hypothesis[ghostwriter] does not require attrs). pip install attrs $PYTEST tests/ghostwriter/ pip uninstall -y attrs pip uninstall -y black if [ "$HYPOTHESIS_PROFILE" != "crosshair" ] ; then # Crosshair tracer is not compatible with no-gil if [ "$(python -c "import sys; print('free-threading' in sys.version)")" != "True" ] ; then # Run twice, interleaved by other tests, to make it more probable to tickle any problems # from accidentally caching/retaining crosshair proxy objects pip install pytest-repeat pip install -r ../requirements/crosshair.txt # requirements/crosshair.txt pins hypothesis. Re-override it with our local changes pip install . $PYTEST --count=2 --repeat-scope=session tests/numpy tests/crosshair # ...but running twice takes time, don't overdo it $PYTEST tests/array_api else $PYTEST tests/array_api tests/numpy fi fi fi ================================================ FILE: hypothesis-python/scripts/validate_branch_check.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import json import sys from collections import defaultdict from pathlib import Path if __name__ == "__main__": data = [] for p in Path.cwd().glob("branch-check*"): data.extend(json.loads(l) for l in p.read_text("utf-8").splitlines()) checks = defaultdict(set) for d in data: checks[d["name"]].add(d["value"]) if not checks: print("No branches found in the branch-check file?") sys.exit(1) always_true = [] always_false = [] for c, vs in sorted(checks.items()): if len(vs) < 2: v = next(iter(vs)) assert v in (False, True) if v: always_true.append(c) else: always_false.append(c) failure = always_true or always_false if failure: print("Some branches were not properly covered.") if always_true: print() print("The following were always True:") for c in always_true: print(f" * {c}") if always_false: print() print("The following were always False:") for c in always_false: print(f" * {c}") if failure: sys.exit(1) print( f"""Successfully validated {len(checks)} branch{"es" if len(checks) > 1 else ""}.""" ) ================================================ FILE: hypothesis-python/src/_hypothesis_ftz_detector.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ This is a toolkit for determining which module set the "flush to zero" flag. For details, see the docstring and comments in `identify_ftz_culprit()`. This module is defined outside the main Hypothesis namespace so that we can avoid triggering import of Hypothesis itself from each subprocess which must import the worker function. """ import importlib import sys from collections.abc import Callable from typing import TYPE_CHECKING, TypeAlias if TYPE_CHECKING: from multiprocessing import Queue FTZCulprits: TypeAlias = tuple[bool | None, set[str]] KNOWN_EVER_CULPRITS = ( # https://moyix.blogspot.com/2022/09/someones-been-messing-with-my-subnormals.html # fmt: off "archive-pdf-tools", "bgfx-python", "bicleaner-ai-glove", "BTrees", "cadbiom", "ctranslate2", "dyNET", "dyNET38", "gevent", "glove-python-binary", "higra", "hybridq", "ikomia", "ioh", "jij-cimod", "lavavu", "lavavu-osmesa", "MulticoreTSNE", "neural-compressor", "nwhy", "openjij", "openturns", "perfmetrics", "pHashPy", "pyace-lite", "pyapr", "pycompadre", "pycompadre-serial", "PyKEP", "pykep", "pylimer-tools", "pyqubo", "pyscf", "PyTAT", "python-prtree", "qiskit-aer", "qiskit-aer-gpu", "RelStorage", "sail-ml", "segmentation", "sente", "sinr", "snapml", "superman", "symengine", "systran-align", "texture-tool", "tsne-mp", "xcsf", # fmt: on ) def flush_to_zero() -> bool: # If this subnormal number compares equal to zero we have a problem return 2.0**-1073 == 0 def run_in_process(fn: Callable[..., FTZCulprits], *args: object) -> FTZCulprits: import multiprocessing as mp mp.set_start_method("spawn", force=True) q: Queue[FTZCulprits] = mp.Queue() p = mp.Process(target=target, args=(q, fn, *args)) p.start() retval = q.get() p.join() return retval def target( q: "Queue[FTZCulprits]", fn: Callable[..., FTZCulprits], *args: object ) -> None: q.put(fn(*args)) def always_imported_modules() -> FTZCulprits: return flush_to_zero(), set(sys.modules) def modules_imported_by(mod: str) -> FTZCulprits: """Return the set of modules imported transitively by mod.""" before = set(sys.modules) try: importlib.import_module(mod) except Exception: return None, set() imports = set(sys.modules) - before return flush_to_zero(), imports # We don't want to redo all the expensive process-spawning checks when we've already # done them, so we cache known-good packages and a known-FTZ result if we have one. KNOWN_FTZ = None CHECKED_CACHE = set() def identify_ftz_culprits() -> str: """Find the modules in sys.modules which cause "mod" to be imported.""" # If we've run this function before, return the same result. global KNOWN_FTZ if KNOWN_FTZ: return KNOWN_FTZ # Start by determining our baseline: the FTZ and sys.modules state in a fresh # process which has only imported this module and nothing else. always_enables_ftz, always_imports = run_in_process(always_imported_modules) if always_enables_ftz: raise RuntimeError("Python is always in FTZ mode, even without imports!") CHECKED_CACHE.update(always_imports) # Next, we'll search through sys.modules looking for a package (or packages) such # that importing them in a new process sets the FTZ state. As a heuristic, we'll # start with packages known to have ever enabled FTZ, then top-level packages as # a way to eliminate large fractions of the search space relatively quickly. def key(name: str) -> tuple[bool, int, str]: """Prefer known-FTZ modules, then top-level packages, then alphabetical.""" return (name not in KNOWN_EVER_CULPRITS, name.count("."), name) # We'll track the set of modules to be checked, and those which do trigger FTZ. candidates = set(sys.modules) - CHECKED_CACHE triggering_modules = {} while candidates: mod = min(candidates, key=key) candidates.discard(mod) enables_ftz, imports = run_in_process(modules_imported_by, mod) imports -= CHECKED_CACHE if enables_ftz: triggering_modules[mod] = imports candidates &= imports else: candidates -= imports CHECKED_CACHE.update(imports) # We only want to report the 'top level' packages which enable FTZ - for example, # if the enabling code is in `a.b`, and `a` in turn imports `a.b`, we prefer to # report `a`. On the other hand, if `a` does _not_ import `a.b`, as is the case # for `hypothesis.extra.*` modules, then `a` will not be in `triggering_modules` # and we'll report `a.b` here instead. prefixes = tuple(n + "." for n in triggering_modules) result = {k for k in triggering_modules if not k.startswith(prefixes)} # Suppose that `bar` enables FTZ, and `foo` imports `bar`. At this point we're # tracking both, but only want to report the latter. for a in sorted(result): for b in sorted(result): if a in triggering_modules[b] and b not in triggering_modules[a]: result.discard(b) # There may be a cyclic dependency which that didn't handle, or simply two # separate modules which both enable FTZ. We already gave up comprehensive # reporting for speed above (`candidates &= imports`), so we'll also buy # simpler reporting by arbitrarily selecting the alphabetically first package. KNOWN_FTZ = min(result) # Cache the result - it's likely this will trigger again! return KNOWN_FTZ if __name__ == "__main__": # This would be really really annoying to write automated tests for, so I've # done some manual exploratory testing: `pip install grequests gevent==21.12.0`, # and call print() as desired to observe behavior. import grequests # noqa # To test without skipping to a known answer, uncomment the following line and # change the last element of key from `name` to `-len(name)` so that we check # grequests before gevent. # KNOWN_EVER_CULPRITS = [c for c in KNOWN_EVER_CULPRITS if c != "gevent"] print(identify_ftz_culprits()) ================================================ FILE: hypothesis-python/src/_hypothesis_globals.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ Module for globals shared between plugin(s) and the main hypothesis module, without depending on either. This file should have no imports outside of stdlib. """ import os in_initialization = 1 """If >0, indicates that hypothesis is still initializing (importing or loading the test environment). `import hypothesis` will cause this number to be decremented, and the pytest plugin increments at load time, then decrements it just before each test session starts. However, this leads to a hole in coverage if another pytest plugin imports hypothesis before our plugin is loaded. HYPOTHESIS_EXTEND_INITIALIZATION may be set to pre-increment the value on behalf of _hypothesis_pytestplugin, plugging the hole.""" if os.environ.get("HYPOTHESIS_EXTEND_INITIALIZATION"): in_initialization += 1 ================================================ FILE: hypothesis-python/src/_hypothesis_pytestplugin.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ The pytest plugin for Hypothesis. We move this from the old location at `hypothesis.extra.pytestplugin` so that it can be loaded by Pytest without importing Hypothesis. In turn, this means that Hypothesis will not load our own third-party plugins (with associated side-effects) unless and until the user explicitly runs `import hypothesis`. See https://github.com/HypothesisWorks/hypothesis/issues/3140 for details. """ import base64 import json import os import sys import warnings from fnmatch import fnmatch from inspect import signature import _hypothesis_globals import pytest try: from _pytest.junitxml import xml_key except ImportError: xml_key = "_xml" # type: ignore LOAD_PROFILE_OPTION = "--hypothesis-profile" VERBOSITY_OPTION = "--hypothesis-verbosity" PRINT_STATISTICS_OPTION = "--hypothesis-show-statistics" SEED_OPTION = "--hypothesis-seed" EXPLAIN_OPTION = "--hypothesis-explain" _VERBOSITY_NAMES = ["quiet", "normal", "verbose", "debug"] _ALL_OPTIONS = [ LOAD_PROFILE_OPTION, VERBOSITY_OPTION, PRINT_STATISTICS_OPTION, SEED_OPTION, EXPLAIN_OPTION, ] STATS_KEY = "_hypothesis_stats" FAILING_EXAMPLES_KEY = "_hypothesis_failing_examples" class StoringReporter: def __init__(self, config): assert "hypothesis" in sys.modules from hypothesis.reporting import default self.report = default self.config = config self.results = [] def __call__(self, msg): if self.config.getoption("capture", "fd") == "no": self.report(msg) if not isinstance(msg, str): msg = repr(msg) self.results.append(msg) # Avoiding distutils.version.LooseVersion due to # https://github.com/HypothesisWorks/hypothesis/issues/2490 if tuple(map(int, pytest.__version__.split(".")[:2])) < (4, 6): # pragma: no cover import warnings PYTEST_TOO_OLD_MESSAGE = """ You are using pytest version %s. Hypothesis tests work with any test runner, but our pytest plugin requires pytest 4.6 or newer. Note that the pytest developers no longer support your version either! Disabling the Hypothesis pytest plugin... """ warnings.warn(PYTEST_TOO_OLD_MESSAGE % (pytest.__version__,), stacklevel=1) else: # Restart side-effect detection as early as possible, to maximize coverage. We # need balanced increment/decrement in configure/sessionstart to support nested # pytest (e.g. runpytest_inprocess), so this early increment in effect replaces # the first one in pytest_configure. if not os.environ.get("HYPOTHESIS_EXTEND_INITIALIZATION"): _hypothesis_globals.in_initialization += 1 if "hypothesis" in sys.modules: # Some other plugin has imported hypothesis, so we'll check if there # have been undetected side-effects and warn if so. from hypothesis.configuration import notice_initialization_restarted notice_initialization_restarted() def pytest_addoption(parser): group = parser.getgroup("hypothesis", "Hypothesis") group.addoption( LOAD_PROFILE_OPTION, action="store", help="Load in a registered hypothesis.settings profile", ) group.addoption( VERBOSITY_OPTION, action="store", choices=_VERBOSITY_NAMES, help="Override profile with verbosity setting specified", ) group.addoption( PRINT_STATISTICS_OPTION, action="store_true", help="Configure when statistics are printed", default=False, ) group.addoption( SEED_OPTION, action="store", help="Set a seed to use for all Hypothesis tests", ) group.addoption( EXPLAIN_OPTION, action="store_true", help="Enable the `explain` phase for failing Hypothesis tests", default=False, ) def _any_hypothesis_option(config): return bool(any(config.getoption(opt) for opt in _ALL_OPTIONS)) def pytest_report_header(config): if not ( config.option.verbose >= 1 or "hypothesis" in sys.modules or _any_hypothesis_option(config) ): return None from hypothesis import Verbosity, settings if config.option.verbose < 1 and settings.default.verbosity < Verbosity.verbose: return None settings_str = settings.default.show_changed() if settings_str != "": settings_str = f" -> {settings_str}" return ( f"hypothesis profile {settings.get_current_profile_name()!r}{settings_str}" ) def pytest_configure(config): config.addinivalue_line("markers", "hypothesis: Tests which use hypothesis.") if not _any_hypothesis_option(config): return from hypothesis import Phase, Verbosity, core, settings profile = config.getoption(LOAD_PROFILE_OPTION) if profile: settings.load_profile(profile) verbosity_name = config.getoption(VERBOSITY_OPTION) if verbosity_name and verbosity_name != settings.default.verbosity.name: verbosity_value = Verbosity[verbosity_name] name = ( f"{settings.get_current_profile_name()}-with-{verbosity_name}-verbosity" ) # register_profile creates a new profile, exactly like the current one, # with the extra values given (in this case 'verbosity') settings.register_profile(name, verbosity=verbosity_value) settings.load_profile(name) if ( config.getoption(EXPLAIN_OPTION) and Phase.explain not in settings.default.phases ): name = f"{settings.get_current_profile_name()}-with-explain-phase" phases = (*settings.default.phases, Phase.explain) settings.register_profile(name, phases=phases) settings.load_profile(name) seed = config.getoption(SEED_OPTION) if seed is not None: try: seed = int(seed) except ValueError: pass core.global_force_seed = seed @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): __tracebackhide__ = True if not (hasattr(item, "obj") and "hypothesis" in sys.modules): yield return from hypothesis import core, is_hypothesis_test # See https://github.com/pytest-dev/pytest/issues/9159 core.pytest_shows_exceptiongroups = ( getattr(pytest, "version_tuple", ())[:2] >= (7, 2) or item.config.getoption("tbstyle", "auto") == "native" ) core.running_under_pytest = True if not is_hypothesis_test(item.obj): # If @given was not applied, check whether other hypothesis # decorators were applied, and raise an error if they were. # We add this frame of indirection to enable __tracebackhide__. def raise_hypothesis_usage_error(msg): raise InvalidArgument(msg) if getattr(item.obj, "is_hypothesis_strategy_function", False): from hypothesis.errors import InvalidArgument raise_hypothesis_usage_error( f"{item.nodeid} is a function that returns a Hypothesis strategy, " "but pytest has collected it as a test function. This is useless " "as the function body will never be executed. To define a test " "function, use @given instead of @composite." ) message = "Using `@%s` on a test without `@given` is completely pointless." for name, attribute in [ ("example", "hypothesis_explicit_examples"), ("seed", "_hypothesis_internal_use_seed"), ("settings", "_hypothesis_internal_settings_applied"), ("reproduce_example", "_hypothesis_internal_use_reproduce_failure"), ]: if hasattr(item.obj, attribute): from hypothesis.errors import InvalidArgument raise_hypothesis_usage_error(message % (name,)) yield return from hypothesis import HealthCheck, settings as Settings from hypothesis.internal.escalation import current_pytest_item from hypothesis.internal.healthcheck import fail_health_check from hypothesis.reporting import with_reporter from hypothesis.statistics import collector, describe_statistics # Retrieve the settings for this test from the test object, which # is normally a Hypothesis wrapped_test wrapper. If this doesn't # work, the test object is probably something weird # (e.g a stateful test wrapper), so we skip the function-scoped # fixture check. settings = getattr( item.obj, "_hypothesis_internal_use_settings", Settings.default ) # Check for suspicious use of function-scoped fixtures, but only # if the corresponding health check is not suppressed. fixture_params = False if not set(settings.suppress_health_check).issuperset( {HealthCheck.function_scoped_fixture, HealthCheck.differing_executors} ): # Warn about function-scoped fixtures, excluding autouse fixtures because # the advice is probably not actionable and the status quo seems OK... # See https://github.com/HypothesisWorks/hypothesis/issues/377 for detail. argnames = None for fx_defs in item._request._fixturemanager.getfixtureinfo( node=item, func=item.function, cls=None ).name2fixturedefs.values(): if argnames is None: argnames = frozenset(signature(item.function).parameters) for fx in fx_defs: fixture_params |= bool(fx.params) if fx.argname not in argnames: continue active_fx = item._request._get_active_fixturedef(fx.argname) if active_fx.scope == "function": fail_health_check( settings, f"{item.nodeid!r} uses a function-scoped fixture {fx.argname!r}." "\n\n" "Function-scoped fixtures are not reset between inputs " "generated by `@given(...)`, which is often surprising and " "can cause subtle test bugs." "\n\n" "If you were expecting the fixture to run separately " "for each generated input, then unfortunately you " "will need to find a different way to achieve your " "goal (for example, replacing the fixture with a similar " "context manager inside of the test)." "\n\n" "If you are confident that your test will work correctly " "even though the fixture is not reset between generated " "inputs, you can suppress this health check with " "@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]). " "See " "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " "for details.", HealthCheck.function_scoped_fixture, ) if fixture_params or (item.get_closest_marker("parametrize") is not None): # Disable the differing_executors health check due to false alarms: # see https://github.com/HypothesisWorks/hypothesis/issues/3733 fn = getattr(item.obj, "__func__", item.obj) fn._hypothesis_internal_use_settings = Settings( parent=settings, suppress_health_check={HealthCheck.differing_executors} | set(settings.suppress_health_check), ) # Give every parametrized test invocation a unique database key key = item.nodeid.encode() item.obj.hypothesis.inner_test._hypothesis_internal_add_digest = key store = StoringReporter(item.config) def note_statistics(stats): stats["nodeid"] = item.nodeid item.hypothesis_statistics = describe_statistics(stats) with ( collector.with_value(note_statistics), with_reporter(store), current_pytest_item.with_value(item), ): yield if store.results: item.hypothesis_report_information = "\n".join(store.results) def _stash_get(config, key, default): if hasattr(config, "stash"): # pytest 7 return config.stash.get(key, default) elif hasattr(config, "_store"): # pytest 5.4 return config._store.get(key, default) else: return getattr(config, key, default) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): report = (yield).get_result() if hasattr(item, "hypothesis_report_information"): report.sections.append(("Hypothesis", item.hypothesis_report_information)) if report.when != "teardown": return terminalreporter = item.config.pluginmanager.getplugin("terminalreporter") if hasattr(item, "hypothesis_statistics"): stats = item.hypothesis_statistics stats_base64 = base64.b64encode(stats.encode()).decode() name = "hypothesis-statistics-" + item.nodeid # Include hypothesis information to the junit XML report. # # Note that when `pytest-xdist` is enabled, `xml_key` is not present in the # stash, so we don't add anything to the junit XML report in that scenario. # https://github.com/pytest-dev/pytest/issues/7767#issuecomment-1082436256 xml = _stash_get(item.config, xml_key, None) if xml: xml.add_global_property(name, stats_base64) # If there's a terminal report, include our summary stats for each test if terminalreporter is not None: report.__dict__[STATS_KEY] = stats # If there's an HTML report, include our summary stats for each test pytest_html = item.config.pluginmanager.getplugin("html") if pytest_html is not None: # pragma: no cover report.extra = [ *getattr(report, "extra", []), pytest_html.extras.text(stats, name="Hypothesis stats"), ] # This doesn't intrinsically have anything to do with the terminalreporter; # we're just cargo-culting a way to get strings back to a single function # even if the test were distributed with pytest-xdist. failing_examples = getattr(item, FAILING_EXAMPLES_KEY, None) if failing_examples and terminalreporter is not None: try: from hypothesis.extra._patching import FAIL_MSG, get_patch_for except ImportError: return # We'll save this as a triple of [filename, hunk_before, hunk_after]. triple = get_patch_for(item.obj, [(x, FAIL_MSG) for x in failing_examples]) if triple is not None: report.__dict__[FAILING_EXAMPLES_KEY] = json.dumps(triple) def pytest_terminal_summary(terminalreporter): failing_examples = [] print_stats = terminalreporter.config.getoption(PRINT_STATISTICS_OPTION) if print_stats: terminalreporter.section("Hypothesis Statistics") for reports in terminalreporter.stats.values(): for report in reports: stats = report.__dict__.get(STATS_KEY) if stats and print_stats: terminalreporter.write_line(stats + "\n\n") examples = report.__dict__.get(FAILING_EXAMPLES_KEY) if examples: failing_examples.append(json.loads(examples)) from hypothesis.internal.observability import _WROTE_TO if _WROTE_TO: terminalreporter.section("Hypothesis") for fname in sorted(_WROTE_TO): terminalreporter.write_line(f"observations written to {fname}") if failing_examples: # This must have been imported already to write the failing examples from hypothesis.extra._patching import gc_patches, make_patch, save_patch patch = make_patch(failing_examples) try: gc_patches() fname = save_patch(patch) except Exception: # fail gracefully if we hit any filesystem or permissions problems return if not _WROTE_TO: terminalreporter.section("Hypothesis") terminalreporter.write_line( f"`git apply {fname}` to add failing examples to your code." ) def pytest_collection_modifyitems(items): if "hypothesis" not in sys.modules: return from hypothesis import is_hypothesis_test has_hypothesis_tests = False for item in items: if isinstance(item, pytest.Function) and is_hypothesis_test(item.obj): item.add_marker("hypothesis") has_hypothesis_tests = True if has_hypothesis_tests: # Collect local constants now, during test collection, so that this # time is not attributed to whatever test happens to run first. # (see https://github.com/HypothesisWorks/hypothesis/issues/4627) # We might miss weird wrapped uses (if there are no other PBTs!), # in order to avoid the latency from inefficient 3rd party plugins. from hypothesis.internal.conjecture.providers import _get_local_constants _get_local_constants() def pytest_sessionstart(session): # Note: may be called multiple times, so we can go negative _hypothesis_globals.in_initialization -= 1 # Monkeypatch some internals to prevent applying @pytest.fixture() to a # function which has already been decorated with @hypothesis.given(). # (the reverse case is already an explicit error in Hypothesis) # We do this here so that it catches people on old Pytest versions too. from _pytest import fixtures def _ban_given_call(self, function): if "hypothesis" in sys.modules: from hypothesis import is_hypothesis_test if is_hypothesis_test(function): raise RuntimeError( f"Can't apply @pytest.fixture() to {function.__name__} because " "it is already decorated with @hypothesis.given()" ) return _orig_call(self, function) _orig_call = fixtures.FixtureFunctionMarker.__call__ fixtures.FixtureFunctionMarker.__call__ = _ban_given_call # type: ignore if int(pytest.__version__.split(".")[0]) >= 7: # pragma: no branch # Hook has had this signature since Pytest 7.0, so skip on older versions def pytest_ignore_collect(collection_path, config): # Detect, warn about, and mititgate certain misconfigurations; # this is mostly educational but can also speed up collection. if ( (name := collection_path.name) == ".hypothesis" and collection_path.is_dir() and not any(fnmatch(name, p) for p in config.getini("norecursedirs")) ): warnings.warn( "Skipping collection of '.hypothesis' directory - this usually " "means you've explicitly set the `norecursedirs` pytest config " "option, replacing rather than extending the default ignores.", stacklevel=1, ) return True return None # let other hooks decide def load(): """Required for `pluggy` to load a plugin from setuptools entrypoints.""" ================================================ FILE: hypothesis-python/src/hypothesis/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Hypothesis is a library for writing unit tests which are parametrized by some source of data. It verifies your code against a wide range of input and minimizes any failing examples it finds. """ import _hypothesis_globals from hypothesis._settings import HealthCheck, Phase, Verbosity, settings from hypothesis.control import ( assume, currently_in_test_context, event, note, reject, target, ) from hypothesis.core import example, find, given, reproduce_failure, seed from hypothesis.entry_points import run from hypothesis.internal.detection import is_hypothesis_test from hypothesis.internal.entropy import register_random from hypothesis.utils.conventions import infer from hypothesis.version import __version__, __version_info__ __all__ = [ "HealthCheck", "Phase", "Verbosity", "__version__", "__version_info__", "assume", "currently_in_test_context", "event", "example", "find", "given", "infer", "is_hypothesis_test", "note", "register_random", "reject", "reproduce_failure", "seed", "settings", "target", ] run() del run _hypothesis_globals.in_initialization -= 1 del _hypothesis_globals ================================================ FILE: hypothesis-python/src/hypothesis/_settings.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """The settings module configures runtime options for Hypothesis. Either an explicit settings object can be used or the default object on this module can be modified. """ import contextlib import datetime import inspect import os from collections.abc import Collection, Generator, Sequence from enum import Enum, EnumMeta, unique from functools import total_ordering from typing import ( TYPE_CHECKING, Any, ClassVar, Optional, TypeVar, ) from hypothesis.errors import ( InvalidArgument, ) from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.internal.validation import check_type, try_convert from hypothesis.utils.conventions import not_set from hypothesis.utils.deprecation import note_deprecation from hypothesis.utils.dynamicvariables import DynamicVariable if TYPE_CHECKING: from hypothesis.database import ExampleDatabase __all__ = ["settings"] T = TypeVar("T") all_settings: list[str] = [ "max_examples", "derandomize", "database", "verbosity", "phases", "stateful_step_count", "report_multiple_bugs", "suppress_health_check", "deadline", "print_blob", "backend", ] @unique @total_ordering class Verbosity(Enum): """Options for the |settings.verbosity| argument to |@settings|.""" quiet = "quiet" """ Hypothesis will not print any output, not even the final falsifying example. """ normal = "normal" """ Standard verbosity. Hypothesis will print the falsifying example, alongside any notes made with |note| (only for the falsfying example). """ verbose = "verbose" """ Increased verbosity. In addition to everything in |Verbosity.normal|, Hypothesis will: * Print each test case as it tries it * Print any notes made with |note| for each test case * Print each shrinking attempt * Print all explicit failing examples when using |@example|, instead of only the simplest one """ debug = "debug" """ Even more verbosity. Useful for debugging Hypothesis internals. You probably don't want this. """ @classmethod def _missing_(cls, value): # deprecation pathway for integer values. Can be removed in Hypothesis 7. if isinstance(value, int) and not isinstance(value, bool): int_to_name = {0: "quiet", 1: "normal", 2: "verbose", 3: "debug"} if value in int_to_name: note_deprecation( f"Passing Verbosity({value}) as an integer is deprecated. " "Hypothesis now treats Verbosity values as strings, not integers. " f"Use Verbosity.{int_to_name[value]} instead.", since="2025-11-05", has_codemod=False, stacklevel=2, ) return cls(int_to_name[value]) return None def __repr__(self) -> str: return f"Verbosity.{self.name}" @staticmethod def _int_value(value: "Verbosity") -> int: # we would just map Verbosity keys, except it's not hashable mapping = { Verbosity.quiet.name: 0, Verbosity.normal.name: 1, Verbosity.verbose.name: 2, Verbosity.debug.name: 3, } # make sure we don't forget any new verbosity members assert list(mapping.keys()) == [verbosity.name for verbosity in Verbosity] return mapping[value.name] def __eq__(self, other: Any) -> bool: if isinstance(other, Verbosity): return super().__eq__(other) return Verbosity._int_value(self) == other def __gt__(self, other: Any) -> bool: value1 = Verbosity._int_value(self) value2 = Verbosity._int_value(other) if isinstance(other, Verbosity) else other return value1 > value2 @unique class Phase(Enum): """Options for the |settings.phases| argument to |@settings|.""" explicit = "explicit" """ Controls whether explicit examples are run. """ reuse = "reuse" """ Controls whether previous examples will be reused. """ generate = "generate" """ Controls whether new examples will be generated. """ target = "target" """ Controls whether examples will be mutated for targeting. """ shrink = "shrink" """ Controls whether examples will be shrunk. """ explain = "explain" """ Controls whether Hypothesis attempts to explain test failures. The explain phase has two parts, each of which is best-effort - if Hypothesis can't find a useful explanation, we'll just print the minimal failing example. """ @classmethod def _missing_(cls, value): # deprecation pathway for integer values. Can be removed in Hypothesis 7. if isinstance(value, int) and not isinstance(value, bool): int_to_name = { 0: "explicit", 1: "reuse", 2: "generate", 3: "target", 4: "shrink", 5: "explain", } if value in int_to_name: note_deprecation( f"Passing Phase({value}) as an integer is deprecated. " "Hypothesis now treats Phase values as strings, not integers. " f"Use Phase.{int_to_name[value]} instead.", since="2025-11-05", has_codemod=False, stacklevel=2, ) return cls(int_to_name[value]) return None def __repr__(self) -> str: return f"Phase.{self.name}" class HealthCheckMeta(EnumMeta): def __iter__(self): deprecated = (HealthCheck.return_value, HealthCheck.not_a_test_method) return iter(x for x in super().__iter__() if x not in deprecated) @unique class HealthCheck(Enum, metaclass=HealthCheckMeta): """ A |HealthCheck| is proactively raised by Hypothesis when Hypothesis detects that your test has performance problems, which may result in less rigorous testing than you expect. For example, if your test takes a long time to generate inputs, or filters away too many inputs using |assume| or |filter|, Hypothesis will raise a corresponding health check. A health check is a proactive warning, not an error. We encourage suppressing health checks where you have evaluated they will not pose a problem, or where you have evaluated that fixing the underlying issue is not worthwhile. With the exception of |HealthCheck.function_scoped_fixture| and |HealthCheck.differing_executors|, all health checks warn about performance problems, not correctness errors. Disabling health checks ----------------------- Health checks can be disabled by |settings.suppress_health_check|. To suppress all health checks, you can pass ``suppress_health_check=list(HealthCheck)``. .. seealso:: See also the :doc:`/how-to/suppress-healthchecks` how-to. Correctness health checks ------------------------- Some health checks report potential correctness errors, rather than performance problems. * |HealthCheck.function_scoped_fixture| indicates that a function-scoped pytest fixture is used by an |@given| test. Many Hypothesis users expect function-scoped fixtures to reset once per input, but they actually reset once per test. We proactively raise |HealthCheck.function_scoped_fixture| to ensure you have considered this case. * |HealthCheck.differing_executors| indicates that the same |@given| test has been executed multiple times with multiple distinct executors. We recommend treating these particular health checks with more care, as suppressing them may result in an unsound test. """ @classmethod def _missing_(cls, value): # deprecation pathway for integer values. Can be removed in Hypothesis 7. if isinstance(value, int) and not isinstance(value, bool): int_to_name = { 1: "data_too_large", 2: "filter_too_much", 3: "too_slow", 5: "return_value", 7: "large_base_example", 8: "not_a_test_method", 9: "function_scoped_fixture", 10: "differing_executors", 11: "nested_given", } if value in int_to_name: note_deprecation( f"Passing HealthCheck({value}) as an integer is deprecated. " "Hypothesis now treats HealthCheck values as strings, not integers. " f"Use HealthCheck.{int_to_name[value]} instead.", since="2025-11-05", has_codemod=False, stacklevel=2, ) return cls(int_to_name[value]) return None def __repr__(self) -> str: return f"{self.__class__.__name__}.{self.name}" @classmethod def all(cls) -> list["HealthCheck"]: # Skipping of deprecated attributes is handled in HealthCheckMeta.__iter__ note_deprecation( "`HealthCheck.all()` is deprecated; use `list(HealthCheck)` instead.", since="2023-04-16", has_codemod=True, stacklevel=1, ) return list(HealthCheck) data_too_large = "data_too_large" """Checks if too many examples are aborted for being too large. This is measured by the number of random choices that Hypothesis makes in order to generate something, not the size of the generated object. For example, choosing a 100MB object from a predefined list would take only a few bits, while generating 10KB of JSON from scratch might trigger this health check. """ filter_too_much = "filter_too_much" """Check for when the test is filtering out too many examples, either through use of |assume| or |.filter|, or occasionally for Hypothesis internal reasons.""" too_slow = "too_slow" """ Check for when input generation is very slow. Since Hypothesis generates 100 (by default) inputs per test execution, a slowdown in generating each input can result in very slow tests overall. """ return_value = "return_value" """Deprecated; we always error if a test returns a non-None value.""" large_base_example = "large_base_example" """ Checks if the smallest natural input to your test is very large. This makes it difficult for Hypothesis to generate good inputs, especially when trying to shrink failing inputs. """ not_a_test_method = "not_a_test_method" """Deprecated; we always error if |@given| is applied to a method defined by :class:`python:unittest.TestCase` (i.e. not a test).""" function_scoped_fixture = "function_scoped_fixture" """Checks if |@given| has been applied to a test with a pytest function-scoped fixture. Function-scoped fixtures run once for the whole function, not once per example, and this is usually not what you want. Because of this limitation, tests that need to set up or reset state for every example need to do so manually within the test itself, typically using an appropriate context manager. Suppress this health check only in the rare case that you are using a function-scoped fixture that does not need to be reset between individual examples, but for some reason you cannot use a wider fixture scope (e.g. session scope, module scope, class scope). This check requires the :ref:`Hypothesis pytest plugin`, which is enabled by default when running Hypothesis inside pytest.""" differing_executors = "differing_executors" """Checks if |@given| has been applied to a test which is executed by different :ref:`executors`. If your test function is defined as a method on a class, that class will be your executor, and subclasses executing an inherited test is a common way for things to go wrong. The correct fix is often to bring the executor instance under the control of hypothesis by explicit parametrization over, or sampling from, subclasses, or to refactor so that |@given| is specified on leaf subclasses.""" nested_given = "nested_given" """Checks if |@given| is used inside another |@given|. This results in quadratic generation and shrinking behavior, and can usually be expressed more cleanly by using :func:`~hypothesis.strategies.data` to replace the inner |@given|. Nesting @given can be appropriate if you set appropriate limits for the quadratic behavior and cannot easily reexpress the inner function with :func:`~hypothesis.strategies.data`. To suppress this health check, set ``suppress_health_check=[HealthCheck.nested_given]`` on the outer |@given|. Setting it on the inner |@given| has no effect. If you have more than one level of nesting, add a suppression for this health check to every |@given| except the innermost one. """ class duration(datetime.timedelta): """A timedelta specifically measured in milliseconds.""" def __repr__(self) -> str: ms = self.total_seconds() * 1000 return f"timedelta(milliseconds={int(ms) if ms == int(ms) else ms!r})" # see https://adamj.eu/tech/2020/03/09/detect-if-your-tests-are-running-on-ci # initially from https://github.com/tox-dev/tox/blob/e911788a/src/tox/util/ci.py _CI_VARS = { "CI": None, # various, including GitHub Actions, Travis CI, and AppVeyor # see https://github.com/tox-dev/tox/issues/3442 "__TOX_ENVIRONMENT_VARIABLE_ORIGINAL_CI": None, "TF_BUILD": "true", # Azure Pipelines "bamboo.buildKey": None, # Bamboo "BUILDKITE": "true", # Buildkite "CIRCLECI": "true", # Circle CI "CIRRUS_CI": "true", # Cirrus CI "CODEBUILD_BUILD_ID": None, # CodeBuild "GITHUB_ACTIONS": "true", # GitHub Actions "GITLAB_CI": None, # GitLab CI "HEROKU_TEST_RUN_ID": None, # Heroku CI "TEAMCITY_VERSION": None, # TeamCity } def is_in_ci() -> bool: return any( key in os.environ and (value is None or os.environ[key] == value) for key, value in _CI_VARS.items() ) default_variable = DynamicVariable[Optional["settings"]](None) def _validate_choices(name: str, value: T, *, choices: Sequence[object]) -> T: if value not in choices: msg = f"Invalid {name}, {value!r}. Valid choices: {choices!r}" raise InvalidArgument(msg) return value def _validate_enum_value(cls: Any, value: object, *, name: str) -> Any: try: return cls(value) except ValueError: raise InvalidArgument( f"{name}={value} is not a valid value. The options " f"are: {', '.join(repr(m.name) for m in cls)}" ) from None def _validate_max_examples(max_examples: int) -> int: check_type(int, max_examples, name="max_examples") if max_examples < 1: raise InvalidArgument( f"max_examples={max_examples!r} must be at least one. If you want " "to disable generation entirely, use phases=[Phase.explicit] instead." ) return max_examples def _validate_database( database: Optional["ExampleDatabase"], ) -> Optional["ExampleDatabase"]: from hypothesis.database import ExampleDatabase if database is None or isinstance(database, ExampleDatabase): return database raise InvalidArgument( "Arguments to the database setting must be None or an instance of " "ExampleDatabase. Use one of the database classes in " "hypothesis.database" ) def _validate_phases(phases: Collection[Phase]) -> Sequence[Phase]: phases = try_convert(tuple, phases, "phases") phases = tuple( _validate_enum_value(Phase, phase, name="phases") for phase in phases ) # sort by definition order return tuple(phase for phase in list(Phase) if phase in phases) def _validate_stateful_step_count(stateful_step_count: int) -> int: check_type(int, stateful_step_count, name="stateful_step_count") if stateful_step_count < 1: raise InvalidArgument( f"stateful_step_count={stateful_step_count!r} must be at least one." ) return stateful_step_count def _validate_suppress_health_check(suppressions: object) -> tuple[HealthCheck, ...]: suppressions = try_convert(tuple, suppressions, "suppress_health_check") for health_check in suppressions: if health_check in (HealthCheck.return_value, HealthCheck.not_a_test_method): note_deprecation( f"The {health_check.name} health check is deprecated, because this is always an error.", since="2023-03-15", has_codemod=False, stacklevel=2, ) return tuple( _validate_enum_value(HealthCheck, health_check, name="suppress_health_check") for health_check in suppressions ) def _validate_deadline( deadline: int | float | datetime.timedelta | None, ) -> duration | None: if deadline is None: return deadline invalid_deadline_error = InvalidArgument( f"deadline={deadline!r} (type {type(deadline).__name__}) must be a timedelta object, " "an integer or float number of milliseconds, or None to disable the " "per-test-case deadline." ) if isinstance(deadline, (int, float)): if isinstance(deadline, bool): raise invalid_deadline_error try: deadline = duration(milliseconds=deadline) except OverflowError: raise InvalidArgument( f"deadline={deadline!r} is invalid, because it is too large to represent " "as a timedelta. Use deadline=None to disable deadlines." ) from None if isinstance(deadline, datetime.timedelta): if deadline <= datetime.timedelta(0): raise InvalidArgument( f"deadline={deadline!r} is invalid, because it is impossible to meet a " "deadline <= 0. Use deadline=None to disable deadlines." ) return duration(seconds=deadline.total_seconds()) raise invalid_deadline_error def _validate_backend(backend: str) -> str: if backend not in AVAILABLE_PROVIDERS: if backend == "crosshair": # pragma: no cover install = '`pip install "hypothesis[crosshair]"` and try again.' raise InvalidArgument(f"backend={backend!r} is not available. {install}") raise InvalidArgument( f"backend={backend!r} is not available - maybe you need to install a plugin?" f"\n Installed backends: {sorted(AVAILABLE_PROVIDERS)!r}" ) return backend class settingsMeta(type): def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) @property def default(cls) -> Optional["settings"]: v = default_variable.value if v is not None: return v if getattr(settings, "_current_profile", None) is not None: assert settings._current_profile is not None settings.load_profile(settings._current_profile) assert default_variable.value is not None return default_variable.value def __setattr__(cls, name: str, value: object) -> None: if name == "default": raise AttributeError( "Cannot assign to the property settings.default - " "consider using settings.load_profile instead." ) elif not name.startswith("_"): raise AttributeError( f"Cannot assign hypothesis.settings.{name}={value!r} - the settings " "class is immutable. You can change the global default " "settings with settings.load_profile, or use @settings(...) " "to decorate your test instead." ) super().__setattr__(name, value) def __repr__(cls): return "hypothesis.settings" class settings(metaclass=settingsMeta): """ A settings object controls the following aspects of test behavior: |~settings.max_examples|, |~settings.derandomize|, |~settings.database|, |~settings.verbosity|, |~settings.phases|, |~settings.stateful_step_count|, |~settings.report_multiple_bugs|, |~settings.suppress_health_check|, |~settings.deadline|, |~settings.print_blob|, and |~settings.backend|. A settings object can be applied as a decorator to a test function, in which case that test function will use those settings. A test may only have one settings object applied to it. A settings object can also be passed to |settings.register_profile| or as a parent to another |settings|. Attribute inheritance --------------------- Settings objects are immutable once created. When a settings object is created, it uses the value specified for each attribute. Any attribute which is not specified will inherit from its value in the ``parent`` settings object. If ``parent`` is not passed, any attributes which are not specified will inherit from the current settings profile instead. For instance, ``settings(max_examples=10)`` will have a ``max_examples`` of ``10``, and the value of all other attributes will be equal to its value in the current settings profile. Changes made from activating a new settings profile with |settings.load_profile| will be reflected in settings objects created after the profile was loaded, but not in existing settings objects. .. _builtin-profiles: Built-in profiles ----------------- While you can register additional profiles with |settings.register_profile|, Hypothesis comes with two built-in profiles: ``default`` and ``ci``. By default, the ``default`` profile is active. If the ``CI`` environment variable is set to any value, the ``ci`` profile is active by default. Hypothesis also automatically detects various vendor-specific CI environment variables. The attributes of the currently active settings profile can be retrieved with ``settings()`` (so ``settings().max_examples`` is the currently active default for |settings.max_examples|). The settings attributes for the built-in profiles are as follows: .. code-block:: python default = settings.register_profile( "default", max_examples=100, derandomize=False, database=not_set, # see settings.database for default behavior verbosity=Verbosity.normal, phases=tuple(Phase), stateful_step_count=50, report_multiple_bugs=True, suppress_health_check=(), deadline=duration(milliseconds=200), print_blob=False, backend="hypothesis", ) ci = settings.register_profile( "ci", parent=default, derandomize=True, deadline=None, database=None, print_blob=True, suppress_health_check=[HealthCheck.too_slow], ) You can replace either of the built-in profiles with |settings.register_profile|: .. code-block:: python # run more examples in CI settings.register_profile( "ci", settings.get_profile("ci"), max_examples=1000, ) """ _profiles: ClassVar[dict[str, "settings"]] = {} _current_profile: ClassVar[str | None] = None def __init__( self, parent: Optional["settings"] = None, *, # This looks pretty strange, but there's good reason: we want Mypy to detect # bad calls downstream, but not to freak out about the `= not_set` part even # though it's not semantically valid to pass that as an argument value. # The intended use is "like **kwargs, but more tractable for tooling". max_examples: int = not_set, # type: ignore derandomize: bool = not_set, # type: ignore database: Optional["ExampleDatabase"] = not_set, # type: ignore verbosity: "Verbosity" = not_set, # type: ignore phases: Collection["Phase"] = not_set, # type: ignore stateful_step_count: int = not_set, # type: ignore report_multiple_bugs: bool = not_set, # type: ignore suppress_health_check: Collection["HealthCheck"] = not_set, # type: ignore deadline: int | float | datetime.timedelta | None = not_set, # type: ignore print_blob: bool = not_set, # type: ignore backend: str = not_set, # type: ignore ) -> None: self._in_definition = True if parent is not None: check_type(settings, parent, "parent") if derandomize not in (not_set, False): if database not in (not_set, None): # type: ignore raise InvalidArgument( "derandomize=True implies database=None, so passing " f"{database=} too is invalid." ) database = None # fallback is None if we're creating the default settings object, and # the parent (or default settings object) otherwise self._fallback = parent or settings.default self._max_examples = ( self._fallback.max_examples # type: ignore if max_examples is not_set # type: ignore else _validate_max_examples(max_examples) ) self._derandomize = ( self._fallback.derandomize # type: ignore if derandomize is not_set # type: ignore else _validate_choices("derandomize", derandomize, choices=[True, False]) ) if database is not not_set: # type: ignore database = _validate_database(database) self._database = database self._cached_database = None self._verbosity = ( self._fallback.verbosity # type: ignore if verbosity is not_set # type: ignore else _validate_enum_value(Verbosity, verbosity, name="verbosity") ) self._phases = ( self._fallback.phases # type: ignore if phases is not_set # type: ignore else _validate_phases(phases) ) self._stateful_step_count = ( self._fallback.stateful_step_count # type: ignore if stateful_step_count is not_set # type: ignore else _validate_stateful_step_count(stateful_step_count) ) self._report_multiple_bugs = ( self._fallback.report_multiple_bugs # type: ignore if report_multiple_bugs is not_set # type: ignore else _validate_choices( "report_multiple_bugs", report_multiple_bugs, choices=[True, False] ) ) self._suppress_health_check = ( self._fallback.suppress_health_check # type: ignore if suppress_health_check is not_set # type: ignore else _validate_suppress_health_check(suppress_health_check) ) self._deadline = ( self._fallback.deadline # type: ignore if deadline is not_set # type: ignore else _validate_deadline(deadline) ) self._print_blob = ( self._fallback.print_blob # type: ignore if print_blob is not_set # type: ignore else _validate_choices("print_blob", print_blob, choices=[True, False]) ) self._backend = ( self._fallback.backend # type: ignore if backend is not_set # type: ignore else _validate_backend(backend) ) self._in_definition = False @property def max_examples(self): """ Once this many satisfying examples have been considered without finding any counter-example, Hypothesis will stop looking. Note that we might call your test function fewer times if we find a bug early or can tell that we've exhausted the search space; or more if we discard some examples due to use of .filter(), assume(), or a few other things that can prevent the test case from completing successfully. The default value is chosen to suit a workflow where the test will be part of a suite that is regularly executed locally or on a CI server, balancing total running time against the chance of missing a bug. If you are writing one-off tests, running tens of thousands of examples is quite reasonable as Hypothesis may miss uncommon bugs with default settings. For very complex code, we have observed Hypothesis finding novel bugs after *several million* examples while testing :pypi:`SymPy `. If you are running more than 100k examples for a test, consider using our :ref:`integration for coverage-guided fuzzing ` - it really shines when given minutes or hours to run. The default max examples is ``100``. """ return self._max_examples @property def derandomize(self): """ If True, seed Hypothesis' random number generator using a hash of the test function, so that every run will test the same set of examples until you update Hypothesis, Python, or the test function. This allows you to `check for regressions and look for bugs `__ using separate settings profiles - for example running quick deterministic tests on every commit, and a longer non-deterministic nightly testing run. The default is ``False``. If running on CI, the default is ``True`` instead. """ return self._derandomize @property def database(self): """ An instance of |ExampleDatabase| that will be used to save examples to and load previous examples from. If not set, a |DirectoryBasedExampleDatabase| is created in the current working directory under ``.hypothesis/examples``. If this location is unusable, e.g. due to the lack of read or write permissions, Hypothesis will emit a warning and fall back to an |InMemoryExampleDatabase|. If ``None``, no storage will be used. See the :ref:`database documentation ` for a list of database classes, and how to define custom database classes. """ from hypothesis.database import _db_for_path # settings.database has two conflicting requirements: # * The default settings should respect changes to set_hypothesis_home_dir # in-between accesses # * `s.database is s.database` should be true, except for the default settings # # We therefore cache s.database for everything except the default settings, # which always recomputes dynamically. if self._fallback is None: # if self._fallback is None, we are the default settings, at which point # we should recompute the database dynamically assert self._database is not_set return _db_for_path(not_set) # otherwise, we cache the database if self._cached_database is None: self._cached_database = ( self._fallback.database if self._database is not_set else self._database ) return self._cached_database @property def verbosity(self): """ Control the verbosity level of Hypothesis messages. To see what's going on while Hypothesis runs your tests, you can turn up the verbosity setting. .. code-block:: pycon >>> from hypothesis import settings, Verbosity >>> from hypothesis.strategies import lists, integers >>> @given(lists(integers())) ... @settings(verbosity=Verbosity.verbose) ... def f(x): ... assert not any(x) ... f() Trying example: [] Falsifying example: [-1198601713, -67, 116, -29578] Shrunk example to [-1198601713] Shrunk example to [-128] Shrunk example to [32] Shrunk example to [1] [1] The four levels are |Verbosity.quiet|, |Verbosity.normal|, |Verbosity.verbose|, and |Verbosity.debug|. |Verbosity.normal| is the default. For |Verbosity.quiet|, Hypothesis will not print anything out, not even the final falsifying example. |Verbosity.debug| is basically |Verbosity.verbose| but a bit more so. You probably don't want it. Verbosity can be passed either as a |Verbosity| enum value, or as the corresponding string value, or as the corresponding integer value. For example: .. code-block:: python # these three are equivalent settings(verbosity=Verbosity.verbose) settings(verbosity="verbose") If you are using :pypi:`pytest`, you may also need to :doc:`disable output capturing for passing tests ` to see verbose output as tests run. """ return self._verbosity @property def phases(self): """ Control which phases should be run. Hypothesis divides tests into logically distinct phases. - |Phase.explicit|: Running explicit examples from |@example|. - |Phase.reuse|: Running examples from the database which previously failed. - |Phase.generate|: Generating new random examples. - |Phase.target|: Mutating examples for :ref:`targeted property-based testing `. Requires |Phase.generate|. - |Phase.shrink|: Shrinking failing examples. - |Phase.explain|: Attempting to explain why a failure occurred. Requires |Phase.shrink|. The phases argument accepts a collection with any subset of these. E.g. ``settings(phases=[Phase.generate, Phase.shrink])`` will generate new examples and shrink them, but will not run explicit examples or reuse previous failures, while ``settings(phases=[Phase.explicit])`` will only run explicit examples from |@example|. Phases can be passed either as a |Phase| enum value, or as the corresponding string value. For example: .. code-block:: python # these two are equivalent settings(phases=[Phase.explicit]) settings(phases=["explicit"]) Following the first failure, Hypothesis will (usually, depending on which |Phase| is enabled) track which lines of code are always run on failing but never on passing inputs. On 3.12+, this uses :mod:`sys.monitoring`, while 3.11 and earlier uses :func:`python:sys.settrace`. For python 3.11 and earlier, we therefore automatically disable the explain phase on PyPy, or if you are using :pypi:`coverage` or a debugger. If there are no clearly suspicious lines of code, :pep:`we refuse the temptation to guess <20>`. After shrinking to a minimal failing example, Hypothesis will try to find parts of the example -- e.g. separate args to |@given| -- which can vary freely without changing the result of that minimal failing example. If the automated experiments run without finding a passing variation, we leave a comment in the final report: .. code-block:: python test_x_divided_by_y( x=0, # or any other generated value y=0, ) Just remember that the *lack* of an explanation sometimes just means that Hypothesis couldn't efficiently find one, not that no explanation (or simpler failing example) exists. """ return self._phases @property def stateful_step_count(self): """ The maximum number of times to call an additional |@rule| method in :ref:`stateful testing ` before we give up on finding a bug. Note that this setting is effectively multiplicative with max_examples, as each example will run for a maximum of ``stateful_step_count`` steps. The default stateful step count is ``50``. """ return self._stateful_step_count @property def report_multiple_bugs(self): """ Because Hypothesis runs the test many times, it can sometimes find multiple bugs in a single run. Reporting all of them at once is usually very useful, but replacing the exceptions can occasionally clash with debuggers. If disabled, only the exception with the smallest minimal example is raised. The default value is ``True``. """ return self._report_multiple_bugs @property def suppress_health_check(self): """ Suppress the given |HealthCheck| exceptions. Those health checks will not be raised by Hypothesis. To suppress all health checks, you can pass ``suppress_health_check=list(HealthCheck)``. Health checks can be passed either as a |HealthCheck| enum value, or as the corresponding string value. For example: .. code-block:: python # these two are equivalent settings(suppress_health_check=[HealthCheck.filter_too_much]) settings(suppress_health_check=["filter_too_much"]) Health checks are proactive warnings, not correctness errors, so we encourage suppressing health checks where you have evaluated they will not pose a problem, or where you have evaluated that fixing the underlying issue is not worthwhile. .. seealso:: See also the :doc:`/how-to/suppress-healthchecks` how-to. """ return self._suppress_health_check @property def deadline(self): """ The maximum allowed duration of an individual test case, in milliseconds. You can pass an integer, float, or timedelta. If ``None``, the deadline is disabled entirely. We treat the deadline as a soft limit in some cases, where that would avoid flakiness due to timing variability. The default deadline is 200 milliseconds. If running on CI, the default is ``None`` instead. """ return self._deadline @property def print_blob(self): """ If set to ``True``, Hypothesis will print code for failing examples that can be used with |@reproduce_failure| to reproduce the failing example. The default value is ``False``. If running on CI, the default is ``True`` instead. """ return self._print_blob @property def backend(self): """ .. warning:: EXPERIMENTAL AND UNSTABLE - see :ref:`alternative-backends`. The importable name of a backend which Hypothesis should use to generate primitive types. We support heuristic-random, solver-based, and fuzzing-based backends. """ return self._backend def __call__(self, test: T) -> T: """Make the settings object (self) an attribute of the test. The settings are later discovered by looking them up on the test itself. """ # Aliasing as Any avoids mypy errors (attr-defined) when accessing and # setting custom attributes on the decorated function or class. _test: Any = test # Using the alias here avoids a mypy error (return-value) later when # ``test`` is returned, because this check results in type refinement. if not callable(_test): raise InvalidArgument( "settings objects can be called as a decorator with @given, " f"but decorated {test=} is not callable." ) if inspect.isclass(test): from hypothesis.stateful import RuleBasedStateMachine if issubclass(_test, RuleBasedStateMachine): attr_name = "_hypothesis_internal_settings_applied" if getattr(test, attr_name, False): raise InvalidArgument( "Applying the @settings decorator twice would " "overwrite the first version; merge their arguments " "instead." ) setattr(test, attr_name, True) _test.TestCase.settings = self return test else: raise InvalidArgument( "@settings(...) can only be used as a decorator on " "functions, or on subclasses of RuleBasedStateMachine." ) if hasattr(_test, "_hypothesis_internal_settings_applied"): # Can't use _hypothesis_internal_use_settings as an indicator that # @settings was applied, because @given also assigns that attribute. descr = get_pretty_function_description(test) raise InvalidArgument( f"{descr} has already been decorated with a settings object.\n" f" Previous: {_test._hypothesis_internal_use_settings!r}\n" f" This: {self!r}" ) _test._hypothesis_internal_use_settings = self _test._hypothesis_internal_settings_applied = True return test def __setattr__(self, name: str, value: object) -> None: if not name.startswith("_") and not self._in_definition: raise AttributeError("settings objects are immutable") return super().__setattr__(name, value) def __repr__(self) -> str: bits = sorted( f"{name}={getattr(self, name)!r}" for name in all_settings if (name != "backend" or len(AVAILABLE_PROVIDERS) > 1) # experimental ) return "settings({})".format(", ".join(bits)) def show_changed(self) -> str: bits = [] for name in all_settings: value = getattr(self, name) if value != getattr(default, name): bits.append(f"{name}={value!r}") return ", ".join(sorted(bits, key=len)) @staticmethod def register_profile( name: str, parent: Optional["settings"] = None, **kwargs: Any, ) -> None: """ Register a settings object as a settings profile, under the name ``name``. The ``parent`` and ``kwargs`` arguments to this method are as for |settings|. If a settings profile already exists under ``name``, it will be overwritten. Registering a profile with the same name as the currently active profile will cause those changes to take effect in the active profile immediately, and do not require reloading the profile. Registered settings profiles can be retrieved later by name with |settings.get_profile|. """ check_type(str, name, "name") if ( default_variable.value and settings._current_profile and default_variable.value != settings._profiles[settings._current_profile] ): note_deprecation( "Cannot register a settings profile when the current settings differ " "from the current profile (usually due to an @settings decorator). " "Register profiles at module level instead.", since="2025-11-15", has_codemod=False, ) # if we just pass the parent and no kwargs, like # settings.register_profile(settings(max_examples=10)) # then optimize out the pointless intermediate settings object which # would just forward everything to the parent. settings._profiles[name] = ( parent if parent is not None and not kwargs else settings(parent=parent, **kwargs) ) if settings._current_profile == name: settings.load_profile(name) @staticmethod def get_profile(name: str) -> "settings": """ Returns the settings profile registered under ``name``. If no settings profile is registered under ``name``, raises |InvalidArgument|. """ check_type(str, name, "name") try: return settings._profiles[name] except KeyError: raise InvalidArgument(f"Profile {name!r} is not registered") from None @staticmethod def load_profile(name: str) -> None: """ Makes the settings profile registered under ``name`` the active profile. If no settings profile is registered under ``name``, raises |InvalidArgument|. """ check_type(str, name, "name") settings._current_profile = name default_variable.value = settings.get_profile(name) @staticmethod def get_current_profile_name() -> str: """ The name of the current settings profile. For example: .. code-block:: python >>> settings.load_profile("myprofile") >>> settings.get_current_profile_name() 'myprofile' """ assert settings._current_profile is not None return settings._current_profile @contextlib.contextmanager def local_settings(s: settings) -> Generator[settings, None, None]: with default_variable.with_value(s): yield s default = settings( max_examples=100, derandomize=False, database=not_set, # type: ignore verbosity=Verbosity.normal, phases=tuple(Phase), stateful_step_count=50, report_multiple_bugs=True, suppress_health_check=(), deadline=duration(milliseconds=200), print_blob=False, backend="hypothesis", ) settings.register_profile("default", default) settings.load_profile("default") assert settings.default is not None CI = settings( derandomize=True, deadline=None, database=None, print_blob=True, suppress_health_check=[HealthCheck.too_slow], ) settings.register_profile("ci", CI) if is_in_ci(): # pragma: no cover # covered in ci, but not locally settings.load_profile("ci") assert settings.default is not None # Check that the kwonly args to settings.__init__ is the same as the set of # defined settings - in case we've added or remove something from one but # not the other. assert set(all_settings) == { p.name for p in inspect.signature(settings.__init__).parameters.values() if p.kind == inspect.Parameter.KEYWORD_ONLY } ================================================ FILE: hypothesis-python/src/hypothesis/configuration.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import sys import warnings from pathlib import Path import _hypothesis_globals from hypothesis.errors import HypothesisSideeffectWarning __hypothesis_home_directory_default = Path.cwd() / ".hypothesis" __hypothesis_home_directory = None def set_hypothesis_home_dir(directory: str | Path | None) -> None: global __hypothesis_home_directory __hypothesis_home_directory = None if directory is None else Path(directory) def storage_directory(*names: str, intent_to_write: bool = True) -> Path: if intent_to_write: check_sideeffect_during_initialization( "accessing storage for {}", "/".join(names) ) global __hypothesis_home_directory if not __hypothesis_home_directory: if where := os.getenv("HYPOTHESIS_STORAGE_DIRECTORY"): __hypothesis_home_directory = Path(where) if not __hypothesis_home_directory: __hypothesis_home_directory = __hypothesis_home_directory_default return __hypothesis_home_directory.joinpath(*names) _first_postinit_what = None def check_sideeffect_during_initialization( what: str, *fmt_args: object, is_restart: bool = False ) -> None: """Called from locations that should not be executed during initialization, for example touching disk or materializing lazy/deferred strategies from plugins. If initialization is in progress, a warning is emitted. Note that computing the repr can take nontrivial time or memory, so we avoid doing so unless (and until) we're actually emitting the warning. """ global _first_postinit_what # This is not a particularly hot path, but neither is it doing productive work, so we want to # minimize the cost by returning immediately. The drawback is that we require # notice_initialization_restarted() to be called if in_initialization changes away from zero. if _first_postinit_what is not None: return elif _hypothesis_globals.in_initialization > 0: msg = what.format(*fmt_args) if is_restart: when = "between importing hypothesis and loading the hypothesis plugin" elif "_hypothesis_pytestplugin" in sys.modules or os.getenv( "HYPOTHESIS_EXTEND_INITIALIZATION" ): when = "during pytest plugin or conftest initialization" else: # pragma: no cover # This can be triggered by Hypothesis plugins, but is really annoying # to test automatically - drop st.text().example() in hypothesis.run() # to manually confirm that we get the warning. when = "at import time" # Note: -Werror is insufficient under pytest, as doesn't take effect until # test session start. text = ( f"Slow code in plugin: avoid {msg} {when}! Set PYTHONWARNINGS=error " "to get a traceback and show which plugin is responsible." ) if is_restart: text += " Additionally, set HYPOTHESIS_EXTEND_INITIALIZATION=1 to pinpoint the exact location." warnings.warn( text, HypothesisSideeffectWarning, stacklevel=3, ) else: _first_postinit_what = (what, fmt_args) def notice_initialization_restarted(*, warn: bool = True) -> None: """Reset _first_postinit_what, so that we don't think we're in post-init. Additionally, if it was set that means that there has been a sideeffect that we haven't warned about, so do that now (the warning text will be correct, and we also hint that the stacktrace can be improved). """ global _first_postinit_what if _first_postinit_what is not None: what, *fmt_args = _first_postinit_what _first_postinit_what = None if warn: check_sideeffect_during_initialization( what, *fmt_args, is_restart=True, ) ================================================ FILE: hypothesis-python/src/hypothesis/control.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import inspect import math import random from collections import defaultdict from collections.abc import Callable, Generator, Sequence from contextlib import contextmanager from typing import Any, Literal, NoReturn, Optional, overload from weakref import WeakKeyDictionary from hypothesis import Verbosity, settings from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption from hypothesis.internal.compat import BaseExceptionGroup from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.observability import observability_enabled from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.internal.validation import check_type from hypothesis.reporting import report, verbose_report from hypothesis.utils.deprecation import note_deprecation from hypothesis.utils.dynamicvariables import DynamicVariable from hypothesis.vendor.pretty import ArgLabelsT, IDKey, PrettyPrintFunction, pretty def _calling_function_location(what: str, frame: Any) -> str: where = frame.f_back return f"{what}() in {where.f_code.co_name} (line {where.f_lineno})" def reject() -> NoReturn: if _current_build_context.value is None: note_deprecation( "Using `reject` outside a property-based test is deprecated", since="2023-09-25", has_codemod=False, ) where = _calling_function_location("reject", inspect.currentframe()) if currently_in_test_context(): counts = current_build_context().data._observability_predicates[where] counts.update_count(condition=False) raise UnsatisfiedAssumption(where) @overload def assume(condition: Literal[False] | None) -> NoReturn: ... @overload def assume(condition: object) -> Literal[True]: ... def assume(condition: object) -> Literal[True]: """Calling ``assume`` is like an :ref:`assert ` that marks the example as bad, rather than failing the test. This allows you to specify properties that you *assume* will be true, and let Hypothesis try to avoid similar examples in future. """ if _current_build_context.value is None: note_deprecation( "Using `assume` outside a property-based test is deprecated", since="2023-09-25", has_codemod=False, ) if observability_enabled() or not condition: where = _calling_function_location("assume", inspect.currentframe()) if observability_enabled() and currently_in_test_context(): counts = current_build_context().data._observability_predicates[where] counts.update_count(condition=bool(condition)) if not condition: raise UnsatisfiedAssumption(f"failed to satisfy {where}") return True _current_build_context = DynamicVariable[Optional["BuildContext"]](None) def currently_in_test_context() -> bool: """Return ``True`` if the calling code is currently running inside an |@given| or :ref:`stateful ` test, and ``False`` otherwise. This is useful for third-party integrations and assertion helpers which may be called from either traditional or property-based tests, and can only use e.g. |assume| or |target| in the latter case. """ return _current_build_context.value is not None def current_build_context() -> "BuildContext": context = _current_build_context.value if context is None: raise InvalidArgument("No build context registered") return context @contextmanager def deprecate_random_in_strategy(fmt, *args): from hypothesis.internal import entropy state_before = random.getstate() yield state_after = random.getstate() if ( # there is a threading race condition here with deterministic_PRNG. Say # we have two threads 1 and 2. We start in global random state A, and # deterministic_PRNG sets to global random state B (which is constant across # threads since we seed to 0 unconditionally). Then we might have state # transitions: # # [1] [2] # A -> B deterministic_PRNG().__enter__ # B ->B deterministic_PRNG().__enter__ # state_before = B deprecate_random_in_strategy.__enter__ # B -> A deterministic_PRNG().__exit__ # state_after = A deprecate_random_in_strategy.__exit__ # # where state_before != state_after because a different thread has reset # the global random state. # # To fix this, we track the known random states set by deterministic_PRNG, # and will not note a deprecation if it matches one of those. state_after != state_before and hash(state_after) not in entropy._known_random_state_hashes ): note_deprecation( "Do not use the `random` module inside strategies; instead " "consider `st.randoms()`, `st.sampled_from()`, etc. " + fmt.format(*args), since="2024-02-05", has_codemod=False, stacklevel=1, ) class BuildContext: def __init__( self, data: ConjectureData, *, is_final: bool = False, wrapped_test: Callable, ) -> None: self.data = data self.tasks: list[Callable[[], Any]] = [] self.is_final = is_final self.wrapped_test = wrapped_test # Use defaultdict(list) here to handle the possibility of having multiple # functions registered for the same object (due to caching, small ints, etc). # The printer will discard duplicates which return different representations. self.known_object_printers: dict[IDKey, list[PrettyPrintFunction]] = ( defaultdict(list) ) # Track nested strategy calls for explain-phase label paths self._label_path: list[str] = [] @contextmanager def track_arg_label(self, label: str) -> Generator[ArgLabelsT, None, None]: start = len(self.data.nodes) self._label_path.append(label) arg_labels: ArgLabelsT = {} try: yield arg_labels finally: self._label_path.pop() # This high up the stack, we can't see or really do much with # Span / SpanRecord - not least because they're only materialized # after the test case is completed. # # Instead, we'll stash the (start_idx, end_idx) pair on our data object # for the ConjectureRunner engine to deal with, and mutate the arg_labels # dict so that the pretty-printer knows where to place the # which-parts-matter comments later. end = len(self.data.nodes) assert start <= end if start != end: arg_labels[label] = (start, end) self.data.arg_slices.add((start, end)) def record_call( self, obj: object, func: object, *, args: Sequence[object], kwargs: dict[str, object], arg_labels: ArgLabelsT | None = None, ) -> None: self.known_object_printers[IDKey(obj)].append( lambda obj, p, cycle, *, _func=func, _arg_labels=arg_labels: p.maybe_repr_known_object_as_call( # type: ignore obj, cycle, get_pretty_function_description(_func), args, kwargs, arg_labels=_arg_labels, ) ) def prep_args_kwargs_from_strategies( self, kwarg_strategies: dict[str, Any], ) -> tuple[dict[str, Any], ArgLabelsT]: arg_labels: ArgLabelsT = {} kwargs: dict[str, Any] = {} for k, s in kwarg_strategies.items(): with ( self.track_arg_label(k) as arg_label, deprecate_random_in_strategy("from {}={!r}", k, s), ): kwargs[k] = self.data.draw(s, observe_as=f"generate:{k}") arg_labels |= arg_label return kwargs, arg_labels def __enter__(self): self.assign_variable = _current_build_context.with_value(self) self.assign_variable.__enter__() return self def __exit__(self, exc_type, exc_value, tb): self.assign_variable.__exit__(exc_type, exc_value, tb) errors = [] for task in self.tasks: try: task() except BaseException as err: errors.append(err) if errors: if len(errors) == 1: raise errors[0] from exc_value raise BaseExceptionGroup("Cleanup failed", errors) from exc_value def cleanup(teardown): """Register a function to be called when the current test has finished executing. Any exceptions thrown in teardown will be printed but not rethrown. Inside a test this isn't very interesting, because you can just use a finally block, but note that you can use this inside map, flatmap, etc. in order to e.g. insist that a value is closed at the end. """ context = _current_build_context.value if context is None: raise InvalidArgument("Cannot register cleanup outside of build context") context.tasks.append(teardown) def should_note(): context = _current_build_context.value if context is None: raise InvalidArgument("Cannot make notes outside of a test") return context.is_final or settings.default.verbosity >= Verbosity.verbose def note(value: object) -> None: """Report this value for the minimal failing example.""" if should_note(): if not isinstance(value, str): value = pretty(value) report(value) def event(value: str, payload: str | int | float = "") -> None: """Record an event that occurred during this test. Statistics on the number of test runs with each event will be reported at the end if you run Hypothesis in statistics reporting mode. Event values should be strings or convertible to them. If an optional payload is given, it will be included in the string for :ref:`statistics`. """ context = _current_build_context.value if context is None: raise InvalidArgument("Cannot record events outside of a test") avoid_realization = context.data.provider.avoid_realization payload = _event_to_string( payload, allowed_types=(str, int, float), avoid_realization=avoid_realization ) value = _event_to_string(value, avoid_realization=avoid_realization) context.data.events[value] = payload _events_to_strings: WeakKeyDictionary = WeakKeyDictionary() def _event_to_string(event, *, allowed_types=str, avoid_realization): if isinstance(event, allowed_types): return event # _events_to_strings is a cache which persists across iterations, causing # problems for symbolic backends. see # https://github.com/pschanely/hypothesis-crosshair/issues/41 if avoid_realization: return str(event) try: return _events_to_strings[event] except (KeyError, TypeError): pass result = str(event) try: _events_to_strings[event] = result except TypeError: pass return result def target(observation: int | float, *, label: str = "") -> int | float: """Calling this function with an ``int`` or ``float`` observation gives it feedback with which to guide our search for inputs that will cause an error, in addition to all the usual heuristics. Observations must always be finite. Hypothesis will try to maximize the observed value over several examples; almost any metric will work so long as it makes sense to increase it. For example, ``-abs(error)`` is a metric that increases as ``error`` approaches zero. Example metrics: - Number of elements in a collection, or tasks in a queue - Mean or maximum runtime of a task (or both, if you use ``label``) - Compression ratio for data (perhaps per-algorithm or per-level) - Number of steps taken by a state machine The optional ``label`` argument can be used to distinguish between and therefore separately optimise distinct observations, such as the mean and standard deviation of a dataset. It is an error to call ``target()`` with any label more than once per test case. .. note:: The more examples you run, the better this technique works. As a rule of thumb, the targeting effect is noticeable above :obj:`max_examples=1000 `, and immediately obvious by around ten thousand examples *per label* used by your test. :ref:`statistics` include the best score seen for each label, which can help avoid `the threshold problem `__ when the minimal example shrinks right down to the threshold of failure (:issue:`2180`). """ check_type((int, float), observation, "observation") if not math.isfinite(observation): raise InvalidArgument(f"{observation=} must be a finite float.") check_type(str, label, "label") context = _current_build_context.value if context is None: raise InvalidArgument( "Calling target() outside of a test is invalid. " "Consider guarding this call with `if currently_in_test_context(): ...`" ) elif context.data.provider.avoid_realization: # We could in principle realize this in the engine, but it seems more # efficient to have our alternative backend optimize it for us. # See e.g. https://github.com/pschanely/hypothesis-crosshair/issues/3 return observation # pragma: no cover verbose_report(f"Saw target({observation!r}, {label=})") if label in context.data.target_observations: raise InvalidArgument( f"Calling target({observation!r}, {label=}) would overwrite " f"target({context.data.target_observations[label]!r}, {label=})" ) else: context.data.target_observations[label] = observation return observation ================================================ FILE: hypothesis-python/src/hypothesis/core.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This module provides the core primitives of Hypothesis, such as given.""" import base64 import contextlib import dataclasses import datetime import inspect import io import math import os import sys import threading import time import traceback import types import unittest import warnings import zlib from collections import defaultdict from collections.abc import Callable, Coroutine, Generator, Hashable, Iterable, Sequence from dataclasses import dataclass, field from functools import partial from inspect import Parameter from random import Random from threading import Lock from types import EllipsisType from typing import ( Any, BinaryIO, TypeVar, overload, ) from unittest import TestCase from hypothesis import strategies as st from hypothesis._settings import ( HealthCheck, Phase, Verbosity, all_settings, local_settings, settings as Settings, ) from hypothesis.control import BuildContext, currently_in_test_context from hypothesis.database import choices_from_bytes, choices_to_bytes from hypothesis.errors import ( BackendCannotProceed, DeadlineExceeded, DidNotReproduce, FailedHealthCheck, FlakyFailure, FlakyReplay, Found, Frozen, HypothesisException, HypothesisWarning, InvalidArgument, NoSuchExample, StopTest, Unsatisfiable, UnsatisfiedAssumption, ) from hypothesis.internal import observability from hypothesis.internal.compat import ( PYPY, BaseExceptionGroup, add_note, bad_django_TestCase, get_type_hints, int_from_bytes, ) from hypothesis.internal.conjecture.choice import ChoiceT from hypothesis.internal.conjecture.data import ConjectureData, Status from hypothesis.internal.conjecture.engine import BUFFER_SIZE, ConjectureRunner from hypothesis.internal.conjecture.junkdrawer import ( ensure_free_stackframes, gc_cumulative_time, ) from hypothesis.internal.conjecture.providers import ( BytestringProvider, PrimitiveProvider, ) from hypothesis.internal.conjecture.shrinker import sort_key from hypothesis.internal.entropy import deterministic_PRNG from hypothesis.internal.escalation import ( InterestingOrigin, current_pytest_item, format_exception, get_trimmed_traceback, is_hypothesis_file, ) from hypothesis.internal.healthcheck import fail_health_check from hypothesis.internal.observability import ( InfoObservation, InfoObservationType, deliver_observation, make_testcase, observability_enabled, ) from hypothesis.internal.reflection import ( convert_positional_arguments, define_function_signature, function_digest, get_pretty_function_description, get_signature, impersonate, is_mock, nicerepr, proxies, repr_call, ) from hypothesis.internal.scrutineer import ( MONITORING_TOOL_ID, Trace, Tracer, explanatory_lines, tractable_coverage_report, ) from hypothesis.internal.validation import check_type from hypothesis.reporting import ( current_verbosity, report, verbose_report, with_reporter, ) from hypothesis.statistics import describe_statistics, describe_targets, note_statistics from hypothesis.strategies._internal.misc import NOTHING from hypothesis.strategies._internal.strategies import ( Ex, SearchStrategy, check_strategy, ) from hypothesis.utils.conventions import not_set from hypothesis.utils.threading import ThreadLocal from hypothesis.vendor.pretty import RepresentationPrinter from hypothesis.version import __version__ TestFunc = TypeVar("TestFunc", bound=Callable) running_under_pytest = False pytest_shows_exceptiongroups = True global_force_seed = None # this variable stores "engine-global" constants, which are global relative to a # ConjectureRunner instance (roughly speaking). Since only one conjecture runner # instance can be active per thread, making engine constants thread-local prevents # the ConjectureRunner instances of concurrent threads from treading on each other. threadlocal = ThreadLocal(_hypothesis_global_random=lambda: None) @dataclass(slots=True, frozen=False) class Example: args: Any kwargs: Any # Plus two optional arguments for .xfail() raises: Any = field(default=None) reason: Any = field(default=None) @dataclass(slots=True, frozen=True) class ReportableError: fragments: list[str] exception: BaseException # TODO_DOCS link to not-yet-existent patch-dumping docs class example: """ Add an explicit input to a Hypothesis test, which Hypothesis will always try before generating random inputs. This combines the randomized nature of Hypothesis generation with a traditional parametrized test. For example: .. code-block:: python @example("Hello world") @example("some string with special significance") @given(st.text()) def test_strings(s): pass will call ``test_strings("Hello World")`` and ``test_strings("some string with special significance")`` before generating any random inputs. |@example| may be placed in any order relative to |@given| and |@settings|. Explicit inputs from |@example| are run in the |Phase.explicit| phase. Explicit inputs do not count towards |settings.max_examples|. Note that explicit inputs added by |@example| do not shrink. If an explicit input fails, Hypothesis will stop and report the failure without generating any random inputs. |@example| can also be used to easily reproduce a failure. For instance, if Hypothesis reports that ``f(n=[0, math.nan])`` fails, you can add ``@example(n=[0, math.nan])`` to your test to quickly reproduce that failure. Arguments to ``@example`` ------------------------- Arguments to |@example| have the same behavior and restrictions as arguments to |@given|. This means they may be either positional or keyword arguments (but not both in the same |@example|): .. code-block:: python @example(1, 2) @example(x=1, y=2) @given(st.integers(), st.integers()) def test(x, y): pass Noting that while arguments to |@given| are strategies (like |st.integers|), arguments to |@example| are values instead (like ``1``). See the :ref:`given-arguments` section for full details. """ def __init__(self, *args: Any, **kwargs: Any) -> None: if args and kwargs: raise InvalidArgument( "Cannot mix positional and keyword arguments for examples" ) if not (args or kwargs): raise InvalidArgument("An example must provide at least one argument") self.hypothesis_explicit_examples: list[Example] = [] self._this_example = Example(tuple(args), kwargs) def __call__(self, test: TestFunc) -> TestFunc: if not hasattr(test, "hypothesis_explicit_examples"): test.hypothesis_explicit_examples = self.hypothesis_explicit_examples # type: ignore test.hypothesis_explicit_examples.append(self._this_example) # type: ignore return test def xfail( self, condition: bool = True, # noqa: FBT002 *, reason: str = "", raises: type[BaseException] | tuple[type[BaseException], ...] = BaseException, ) -> "example": """Mark this example as an expected failure, similarly to :obj:`pytest.mark.xfail(strict=True) `. Expected-failing examples allow you to check that your test does fail on some examples, and therefore build confidence that *passing* tests are because your code is working, not because the test is missing something. .. code-block:: python @example(...).xfail() @example(...).xfail(reason="Prices must be non-negative") @example(...).xfail(raises=(KeyError, ValueError)) @example(...).xfail(sys.version_info[:2] >= (3, 12), reason="needs py 3.12") @example(...).xfail(condition=sys.platform != "linux", raises=OSError) def test(x): pass .. note:: Expected-failing examples are handled separately from those generated by strategies, so you should usually ensure that there is no overlap. .. code-block:: python @example(x=1, y=0).xfail(raises=ZeroDivisionError) @given(x=st.just(1), y=st.integers()) # Missing `.filter(bool)`! def test_fraction(x, y): # This test will try the explicit example and see it fail as # expected, then go on to generate more examples from the # strategy. If we happen to generate y=0, the test will fail # because only the explicit example is treated as xfailing. x / y """ check_type(bool, condition, "condition") check_type(str, reason, "reason") if not ( isinstance(raises, type) and issubclass(raises, BaseException) ) and not ( isinstance(raises, tuple) and raises # () -> expected to fail with no error, which is impossible and all( isinstance(r, type) and issubclass(r, BaseException) for r in raises ) ): raise InvalidArgument( f"{raises=} must be an exception type or tuple of exception types" ) if condition: self._this_example = dataclasses.replace( self._this_example, raises=raises, reason=reason ) return self def via(self, whence: str, /) -> "example": """Attach a machine-readable label noting what the origin of this example was. |example.via| is completely optional and does not change runtime behavior. |example.via| is intended to support self-documenting behavior, as well as tooling which might add (or remove) |@example| decorators automatically. For example: .. code-block:: python # Annotating examples is optional and does not change runtime behavior @example(...) @example(...).via("regression test for issue #42") @example(...).via("discovered failure") def test(x): pass .. note:: `HypoFuzz `_ uses |example.via| to tag examples in the patch of its high-coverage set of explicit inputs, on `the patches page `_. """ if not isinstance(whence, str): raise InvalidArgument(".via() must be passed a string") # This is deliberately a no-op at runtime; the tools operate on source code. return self def seed(seed: Hashable) -> Callable[[TestFunc], TestFunc]: """ Seed the randomness for this test. ``seed`` may be any hashable object. No exact meaning for ``seed`` is provided other than that for a fixed seed value Hypothesis will produce the same examples (assuming that there are no other sources of nondeterminisim, such as timing, hash randomization, or external state). For example, the following test function and |RuleBasedStateMachine| will each generate the same series of examples each time they are executed: .. code-block:: python @seed(1234) @given(st.integers()) def test(n): ... @seed(6789) class MyMachine(RuleBasedStateMachine): ... If using pytest, you can alternatively pass ``--hypothesis-seed`` on the command line. Setting a seed overrides |settings.derandomize|, which is designed to enable deterministic CI tests rather than reproducing observed failures. Hypothesis will only print the seed which would reproduce a failure if a test fails in an unexpected way, for instance inside Hypothesis internals. """ def accept(test): test._hypothesis_internal_use_seed = seed current_settings = getattr(test, "_hypothesis_internal_use_settings", None) test._hypothesis_internal_use_settings = Settings( current_settings, database=None ) return test return accept # TODO_DOCS: link to /explanation/choice-sequence def reproduce_failure(version: str, blob: bytes) -> Callable[[TestFunc], TestFunc]: """ Run the example corresponding to the binary ``blob`` in order to reproduce a failure. ``blob`` is a serialized version of the internal input representation of Hypothesis. A test decorated with |@reproduce_failure| always runs exactly one example, which is expected to cause a failure. If the provided ``blob`` does not cause a failure, Hypothesis will raise |DidNotReproduce|. Hypothesis will print an |@reproduce_failure| decorator if |settings.print_blob| is ``True`` (which is the default in CI). |@reproduce_failure| is intended to be temporarily added to your test suite in order to reproduce a failure. It is not intended to be a permanent addition to your test suite. Because of this, no compatibility guarantees are made across Hypothesis versions, and |@reproduce_failure| will error if used on a different Hypothesis version than it was created for. .. seealso:: See also the :doc:`/tutorial/replaying-failures` tutorial. """ def accept(test): test._hypothesis_internal_use_reproduce_failure = (version, blob) return test return accept def reproduction_decorator(choices: Iterable[ChoiceT]) -> str: return f"@reproduce_failure({__version__!r}, {encode_failure(choices)!r})" def encode_failure(choices: Iterable[ChoiceT]) -> bytes: blob = choices_to_bytes(choices) compressed = zlib.compress(blob) if len(compressed) < len(blob): blob = b"\1" + compressed else: blob = b"\0" + blob return base64.b64encode(blob) def decode_failure(blob: bytes) -> Sequence[ChoiceT]: try: decoded = base64.b64decode(blob) except Exception: raise InvalidArgument(f"Invalid base64 encoded string: {blob!r}") from None prefix = decoded[:1] if prefix == b"\0": decoded = decoded[1:] elif prefix == b"\1": try: decoded = zlib.decompress(decoded[1:]) except zlib.error as err: raise InvalidArgument( f"Invalid zlib compression for blob {blob!r}" ) from err else: raise InvalidArgument( f"Could not decode blob {blob!r}: Invalid start byte {prefix!r}" ) choices = choices_from_bytes(decoded) if choices is None: raise InvalidArgument(f"Invalid serialized choice sequence for blob {blob!r}") return choices def _invalid(message, *, exc=InvalidArgument, test, given_kwargs): @impersonate(test) def wrapped_test(*arguments, **kwargs): # pragma: no cover # coverage limitation raise exc(message) wrapped_test.is_hypothesis_test = True wrapped_test.hypothesis = HypothesisHandle( inner_test=test, _get_fuzz_target=wrapped_test, _given_kwargs=given_kwargs, ) return wrapped_test def is_invalid_test(test, original_sig, given_arguments, given_kwargs): """Check the arguments to ``@given`` for basic usage constraints. Most errors are not raised immediately; instead we return a dummy test function that will raise the appropriate error if it is actually called. When the user runs a subset of tests (e.g via ``pytest -k``), errors will only be reported for tests that actually ran. """ invalid = partial(_invalid, test=test, given_kwargs=given_kwargs) if not (given_arguments or given_kwargs): return invalid("given must be called with at least one argument") params = list(original_sig.parameters.values()) pos_params = [p for p in params if p.kind is p.POSITIONAL_OR_KEYWORD] kwonly_params = [p for p in params if p.kind is p.KEYWORD_ONLY] if given_arguments and params != pos_params: return invalid( "positional arguments to @given are not supported with varargs, " "varkeywords, positional-only, or keyword-only arguments" ) if len(given_arguments) > len(pos_params): return invalid( f"Too many positional arguments for {test.__name__}() were passed to " f"@given - expected at most {len(pos_params)} " f"arguments, but got {len(given_arguments)} {given_arguments!r}" ) if ... in given_arguments: return invalid( "... was passed as a positional argument to @given, but may only be " "passed as a keyword argument or as the sole argument of @given" ) if given_arguments and given_kwargs: return invalid("cannot mix positional and keyword arguments to @given") extra_kwargs = [ k for k in given_kwargs if k not in {p.name for p in pos_params + kwonly_params} ] if extra_kwargs and (params == [] or params[-1].kind is not params[-1].VAR_KEYWORD): arg = extra_kwargs[0] extra = "" if arg in all_settings: extra = f". Did you mean @settings({arg}={given_kwargs[arg]!r})?" return invalid( f"{test.__name__}() got an unexpected keyword argument {arg!r}, " f"from `{arg}={given_kwargs[arg]!r}` in @given{extra}" ) if any(p.default is not p.empty for p in params): return invalid("Cannot apply @given to a function with defaults.") # This case would raise Unsatisfiable *anyway*, but by detecting it here we can # provide a much more helpful error message for people e.g. using the Ghostwriter. empty = [ f"{s!r} (arg {idx})" for idx, s in enumerate(given_arguments) if s is NOTHING ] + [f"{name}={s!r}" for name, s in given_kwargs.items() if s is NOTHING] if empty: strats = "strategies" if len(empty) > 1 else "strategy" return invalid( f"Cannot generate examples from empty {strats}: " + ", ".join(empty), exc=Unsatisfiable, ) def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_sig): assert isinstance(state, StateForActualGivenExecution) posargs = [ p.name for p in original_sig.parameters.values() if p.kind is p.POSITIONAL_OR_KEYWORD ] for example in reversed(getattr(wrapped_test, "hypothesis_explicit_examples", ())): assert isinstance(example, Example) # All of this validation is to check that @example() got "the same" arguments # as @given, i.e. corresponding to the same parameters, even though they might # be any mixture of positional and keyword arguments. if example.args: assert not example.kwargs if any( p.kind is p.POSITIONAL_ONLY for p in original_sig.parameters.values() ): raise InvalidArgument( "Cannot pass positional arguments to @example() when decorating " "a test function which has positional-only parameters." ) if len(example.args) > len(posargs): raise InvalidArgument( "example has too many arguments for test. Expected at most " f"{len(posargs)} but got {len(example.args)}" ) example_kwargs = dict( zip(posargs[-len(example.args) :], example.args, strict=True) ) else: example_kwargs = dict(example.kwargs) given_kws = ", ".join( repr(k) for k in sorted(wrapped_test.hypothesis._given_kwargs) ) example_kws = ", ".join(repr(k) for k in sorted(example_kwargs)) if given_kws != example_kws: raise InvalidArgument( f"Inconsistent args: @given() got strategies for {given_kws}, " f"but @example() got arguments for {example_kws}" ) from None # This is certainly true because the example_kwargs exactly match the params # reserved by @given(), which are then remove from the function signature. assert set(example_kwargs).isdisjoint(kwargs) example_kwargs.update(kwargs) if Phase.explicit not in state.settings.phases: continue with local_settings(state.settings): fragments_reported = [] empty_data = ConjectureData.for_choices([]) try: execute_example = partial( state.execute_once, empty_data, is_final=True, print_example=True, example_kwargs=example_kwargs, ) with with_reporter(fragments_reported.append): if example.raises is None: execute_example() else: # @example(...).xfail(...) bits = ", ".join(nicerepr(x) for x in arguments) + ", ".join( f"{k}={nicerepr(v)}" for k, v in example_kwargs.items() ) try: execute_example() except failure_exceptions_to_catch() as err: if not isinstance(err, example.raises): raise # Save a string form of this example; we'll warn if it's # ever generated by the strategy (which can't be xfailed) state.xfail_example_reprs.add( repr_call(state.test, arguments, example_kwargs) ) except example.raises as err: # We'd usually check this as early as possible, but it's # possible for failure_exceptions_to_catch() to grow when # e.g. pytest is imported between import- and test-time. raise InvalidArgument( f"@example({bits}) raised an expected {err!r}, " "but Hypothesis does not treat this as a test failure" ) from err else: # Unexpectedly passing; always raise an error in this case. reason = f" because {example.reason}" * bool(example.reason) if example.raises is BaseException: name = "exception" # special-case no raises= arg elif not isinstance(example.raises, tuple): name = example.raises.__name__ elif len(example.raises) == 1: name = example.raises[0].__name__ else: name = ( ", ".join(ex.__name__ for ex in example.raises[:-1]) + f", or {example.raises[-1].__name__}" ) vowel = name.upper()[0] in "AEIOU" raise AssertionError( f"Expected a{'n' * vowel} {name} from @example({bits})" f"{reason}, but no exception was raised." ) except UnsatisfiedAssumption: # Odd though it seems, we deliberately support explicit examples that # are then rejected by a call to `assume()`. As well as iterative # development, this is rather useful to replay Hypothesis' part of # a saved failure when other arguments are supplied by e.g. pytest. # See https://github.com/HypothesisWorks/hypothesis/issues/2125 with contextlib.suppress(StopTest): empty_data.conclude_test(Status.INVALID) except BaseException as err: # In order to support reporting of multiple failing examples, we yield # each of the (report text, error) pairs we find back to the top-level # runner. This also ensures that user-facing stack traces have as few # frames of Hypothesis internals as possible. err = err.with_traceback(get_trimmed_traceback()) # One user error - whether misunderstanding or typo - we've seen a few # times is to pass strategies to @example() where values are expected. # Checking is easy, and false-positives not much of a problem, so: if isinstance(err, failure_exceptions_to_catch()) and any( isinstance(arg, SearchStrategy) for arg in example.args + tuple(example.kwargs.values()) ): new = HypothesisWarning( "The @example() decorator expects to be passed values, but " "you passed strategies instead. See https://hypothesis." "readthedocs.io/en/latest/reference/api.html#hypothesis" ".example for details." ) new.__cause__ = err err = new with contextlib.suppress(StopTest): empty_data.conclude_test(Status.INVALID) yield ReportableError(fragments_reported, err) if ( state.settings.report_multiple_bugs and pytest_shows_exceptiongroups and isinstance(err, failure_exceptions_to_catch()) and not isinstance(err, skip_exceptions_to_reraise()) ): continue break finally: if fragments_reported: assert fragments_reported[0].startswith("Falsifying example") fragments_reported[0] = fragments_reported[0].replace( "Falsifying example", "Falsifying explicit example", 1 ) empty_data.freeze() if observability_enabled(): tc = make_testcase( run_start=state._start_timestamp, property=state.test_identifier, data=empty_data, how_generated="explicit example", representation=state._string_repr, timing=state._timing_features, ) deliver_observation(tc) if fragments_reported: verbose_report(fragments_reported[0].replace("Falsifying", "Trying", 1)) for f in fragments_reported[1:]: verbose_report(f) def get_random_for_wrapped_test(test, wrapped_test): settings = wrapped_test._hypothesis_internal_use_settings wrapped_test._hypothesis_internal_use_generated_seed = None if wrapped_test._hypothesis_internal_use_seed is not None: return Random(wrapped_test._hypothesis_internal_use_seed) if settings.derandomize: return Random(int_from_bytes(function_digest(test))) if global_force_seed is not None: return Random(global_force_seed) if threadlocal._hypothesis_global_random is None: # pragma: no cover threadlocal._hypothesis_global_random = Random() seed = threadlocal._hypothesis_global_random.getrandbits(128) wrapped_test._hypothesis_internal_use_generated_seed = seed return Random(seed) @dataclass(slots=True, frozen=False) class Stuff: selfy: Any args: tuple kwargs: dict given_kwargs: dict def process_arguments_to_given( wrapped_test: Any, arguments: Sequence[object], kwargs: dict[str, object], given_kwargs: dict[str, SearchStrategy], params: dict[str, Parameter], ) -> tuple[Sequence[object], dict[str, object], Stuff]: selfy = None arguments, kwargs = convert_positional_arguments(wrapped_test, arguments, kwargs) # If the test function is a method of some kind, the bound object # will be the first named argument if there are any, otherwise the # first vararg (if any). posargs = [p.name for p in params.values() if p.kind is p.POSITIONAL_OR_KEYWORD] if posargs: selfy = kwargs.get(posargs[0]) elif arguments: selfy = arguments[0] # Ensure that we don't mistake mocks for self here. # This can cause the mock to be used as the test runner. if is_mock(selfy): selfy = None arguments = tuple(arguments) with ensure_free_stackframes(): for k, s in given_kwargs.items(): check_strategy(s, name=k) s.validate() stuff = Stuff(selfy=selfy, args=arguments, kwargs=kwargs, given_kwargs=given_kwargs) return arguments, kwargs, stuff def skip_exceptions_to_reraise(): """Return a tuple of exceptions meaning 'skip this test', to re-raise. This is intended to cover most common test runners; if you would like another to be added please open an issue or pull request adding it to this function and to tests/cover/test_lazy_import.py """ # This is a set in case any library simply re-exports another's Skip exception exceptions = set() # We use this sys.modules trick to avoid importing libraries - # you can't be an instance of a type from an unimported module! # This is fast enough that we don't need to cache the result, # and more importantly it avoids possible side-effects :-) if "unittest" in sys.modules: exceptions.add(sys.modules["unittest"].SkipTest) if "_pytest.outcomes" in sys.modules: exceptions.add(sys.modules["_pytest.outcomes"].Skipped) return tuple(sorted(exceptions, key=str)) def failure_exceptions_to_catch() -> tuple[type[BaseException], ...]: """Return a tuple of exceptions meaning 'this test has failed', to catch. This is intended to cover most common test runners; if you would like another to be added please open an issue or pull request. """ # While SystemExit and GeneratorExit are instances of BaseException, we also # expect them to be deterministic - unlike KeyboardInterrupt - and so we treat # them as standard exceptions, check for flakiness, etc. # See https://github.com/HypothesisWorks/hypothesis/issues/2223 for details. exceptions = [Exception, SystemExit, GeneratorExit] if "_pytest.outcomes" in sys.modules: exceptions.append(sys.modules["_pytest.outcomes"].Failed) return tuple(exceptions) def new_given_signature(original_sig, given_kwargs): """Make an updated signature for the wrapped test.""" return original_sig.replace( parameters=[ p for p in original_sig.parameters.values() if not ( p.name in given_kwargs and p.kind in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY) ) ], return_annotation=None, ) def default_executor(data, function): return function(data) def get_executor(runner): try: execute_example = runner.execute_example except AttributeError: pass else: return lambda data, function: execute_example(partial(function, data)) if hasattr(runner, "setup_example") or hasattr(runner, "teardown_example"): setup = getattr(runner, "setup_example", None) or (lambda: None) teardown = getattr(runner, "teardown_example", None) or (lambda ex: None) def execute(data, function): token = None try: token = setup() return function(data) finally: teardown(token) return execute return default_executor # This function is a crude solution, a better way of resolving it would probably # be to rewrite a bunch of exception handlers to use except*. T = TypeVar("T", bound=BaseException) def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]: found_exceptions: list[T] = [] for exc in excgroup.exceptions: if isinstance(exc, BaseExceptionGroup): found_exceptions.extend(_flatten_group(exc)) else: found_exceptions.append(exc) return found_exceptions @contextlib.contextmanager def unwrap_markers_from_group() -> Generator[None, None, None]: try: yield except BaseExceptionGroup as excgroup: _frozen_exceptions, non_frozen_exceptions = excgroup.split(Frozen) # group only contains Frozen, reraise the group # it doesn't matter what we raise, since any exceptions get disregarded # and reraised as StopTest if data got frozen. if non_frozen_exceptions is None: raise # in all other cases they are discarded # Can RewindRecursive end up in this group? _, user_exceptions = non_frozen_exceptions.split( lambda e: isinstance(e, (StopTest, HypothesisException)) ) # this might contain marker exceptions, or internal errors, but not frozen. if user_exceptions is not None: raise # single marker exception - reraise it flattened_non_frozen_exceptions: list[BaseException] = _flatten_group( non_frozen_exceptions ) if len(flattened_non_frozen_exceptions) == 1: e = flattened_non_frozen_exceptions[0] # preserve the cause of the original exception to not hinder debugging # note that __context__ is still lost though raise e from e.__cause__ # multiple marker exceptions. If we re-raise the whole group we break # a bunch of logic so ....? stoptests, non_stoptests = non_frozen_exceptions.split(StopTest) # TODO: stoptest+hypothesisexception ...? Is it possible? If so, what do? if non_stoptests: # TODO: multiple marker exceptions is easy to produce, but the logic in the # engine does not handle it... so we just reraise the first one for now. e = _flatten_group(non_stoptests)[0] raise e from e.__cause__ assert stoptests is not None # multiple stoptests: raising the one with the lowest testcounter raise min(_flatten_group(stoptests), key=lambda s_e: s_e.testcounter) class StateForActualGivenExecution: def __init__( self, stuff: Stuff, test: Callable[..., Any], settings: Settings, random: Random, wrapped_test: Any, *, thread_overlap: dict[int, bool] | None = None, ): self.stuff = stuff self.test = test self.settings = settings self.random = random self.wrapped_test = wrapped_test self.thread_overlap = {} if thread_overlap is None else thread_overlap self.test_runner = get_executor(stuff.selfy) self.print_given_args = getattr( wrapped_test, "_hypothesis_internal_print_given_args", True ) self.last_exception = None self.falsifying_examples = () self.ever_executed = False self.xfail_example_reprs: set[str] = set() self.failed_normally = False self.failed_due_to_deadline = False self.explain_traces: dict[None | InterestingOrigin, set[Trace]] = defaultdict( set ) self._start_timestamp = time.time() self._string_repr = "" self._timing_features: dict[str, float] = {} self._runner: ConjectureRunner | None = None @property def test_identifier(self) -> str: return getattr( current_pytest_item.value, "nodeid", None ) or get_pretty_function_description(self.wrapped_test) def _should_trace(self): # NOTE: we explicitly support monkeypatching this. Keep the namespace # access intact. _trace_obs = ( observability_enabled() and observability.OBSERVABILITY_COLLECT_COVERAGE ) _trace_failure = ( self.failed_normally and not self.failed_due_to_deadline and {Phase.shrink, Phase.explain}.issubset(self.settings.phases) ) return _trace_obs or _trace_failure def execute_once( self, data, *, print_example=False, is_final=False, expected_failure=None, example_kwargs=None, ): """Run the test function once, using ``data`` as input. If the test raises an exception, it will propagate through to the caller of this method. Depending on its type, this could represent an ordinary test failure, or a fatal error, or a control exception. If this method returns normally, the test might have passed, or it might have placed ``data`` in an unsuccessful state and then swallowed the corresponding control exception. """ self.ever_executed = True self._string_repr = "" text_repr = None if self.settings.deadline is None and not observability_enabled(): @proxies(self.test) def test(*args, **kwargs): with unwrap_markers_from_group(), ensure_free_stackframes(): return self.test(*args, **kwargs) else: @proxies(self.test) def test(*args, **kwargs): arg_drawtime = math.fsum(data.draw_times.values()) arg_stateful = math.fsum(data._stateful_run_times.values()) arg_gctime = gc_cumulative_time() with unwrap_markers_from_group(), ensure_free_stackframes(): start = time.perf_counter() try: result = self.test(*args, **kwargs) finally: finish = time.perf_counter() in_drawtime = math.fsum(data.draw_times.values()) - arg_drawtime in_stateful = ( math.fsum(data._stateful_run_times.values()) - arg_stateful ) in_gctime = gc_cumulative_time() - arg_gctime runtime = finish - start - in_drawtime - in_stateful - in_gctime self._timing_features = { "execute:test": runtime, "overall:gc": in_gctime, **data.draw_times, **data._stateful_run_times, } if ( (current_deadline := self.settings.deadline) is not None # we disable the deadline check under concurrent threads, since # cpython may switch away from a thread for arbitrarily long. and not self.thread_overlap.get(threading.get_ident(), False) ): if not is_final: current_deadline = (current_deadline // 4) * 5 if runtime >= current_deadline.total_seconds(): raise DeadlineExceeded( datetime.timedelta(seconds=runtime), self.settings.deadline ) return result def run(data: ConjectureData) -> None: # Set up dynamic context needed by a single test run. if self.stuff.selfy is not None: data.hypothesis_runner = self.stuff.selfy # Generate all arguments to the test function. args = self.stuff.args kwargs = dict(self.stuff.kwargs) if example_kwargs is None: kw, argslices = context.prep_args_kwargs_from_strategies( self.stuff.given_kwargs ) else: kw = example_kwargs argslices = {} kwargs.update(kw) if expected_failure is not None: nonlocal text_repr text_repr = repr_call(test, args, kwargs) if print_example or current_verbosity() >= Verbosity.verbose: printer = RepresentationPrinter(context=context) if print_example: printer.text("Falsifying example:") else: printer.text("Trying example:") if self.print_given_args: printer.text(" ") printer.repr_call( test.__name__, args, kwargs, force_split=True, arg_slices=argslices, leading_comment=( "# " + context.data.slice_comments[(0, 0)] if (0, 0) in context.data.slice_comments else None ), avoid_realization=data.provider.avoid_realization, ) report(printer.getvalue()) if observability_enabled(): printer = RepresentationPrinter(context=context) printer.repr_call( test.__name__, args, kwargs, force_split=True, arg_slices=argslices, leading_comment=( "# " + context.data.slice_comments[(0, 0)] if (0, 0) in context.data.slice_comments else None ), avoid_realization=data.provider.avoid_realization, ) self._string_repr = printer.getvalue() try: return test(*args, **kwargs) except TypeError as e: # If we sampled from a sequence of strategies, AND failed with a # TypeError, *AND that exception mentions SearchStrategy*, add a note: if ( "SearchStrategy" in str(e) and data._sampled_from_all_strategies_elements_message is not None ): msg, format_arg = data._sampled_from_all_strategies_elements_message add_note(e, msg.format(format_arg)) raise finally: if data._stateful_repr_parts is not None: self._string_repr = "\n".join(data._stateful_repr_parts) if observability_enabled(): printer = RepresentationPrinter(context=context) for name, value in data._observability_args.items(): if name.startswith("generate:Draw "): try: value = data.provider.realize(value) except BackendCannotProceed: # pragma: no cover value = "" printer.text(f"\n{name.removeprefix('generate:')}: ") printer.pretty(value) self._string_repr += printer.getvalue() # self.test_runner can include the execute_example method, or setup/teardown # _example, so it's important to get the PRNG and build context in place first. with ( local_settings(self.settings), deterministic_PRNG(), BuildContext( data, is_final=is_final, wrapped_test=self.wrapped_test ) as context, ): # providers may throw in per_case_context_fn, and we'd like # `result` to still be set in these cases. result = None with data.provider.per_test_case_context_manager(): # Run the test function once, via the executor hook. # In most cases this will delegate straight to `run(data)`. result = self.test_runner(data, run) # If a failure was expected, it should have been raised already, so # instead raise an appropriate diagnostic error. if expected_failure is not None: exception, traceback = expected_failure if isinstance(exception, DeadlineExceeded) and ( runtime_secs := math.fsum( v for k, v in self._timing_features.items() if k.startswith("execute:") ) ): report( "Unreliable test timings! On an initial run, this " f"test took {exception.runtime.total_seconds() * 1000:.2f}ms, " "which exceeded the deadline of " f"{self.settings.deadline.total_seconds() * 1000:.2f}ms, but " f"on a subsequent run it took {runtime_secs * 1000:.2f} ms, " "which did not. If you expect this sort of " "variability in your test timings, consider turning " "deadlines off for this test by setting deadline=None." ) else: report("Failed to reproduce exception. Expected: \n" + traceback) raise FlakyFailure( f"Hypothesis {text_repr} produces unreliable results: " "Falsified on the first call but did not on a subsequent one", [exception], ) return result def _flaky_replay_to_failure( self, err: FlakyReplay, context: BaseException ) -> FlakyFailure: assert self._runner is not None # Note that in the mark_interesting case, _context_ itself # is part of err._interesting_examples - but it's not in # _runner.interesting_examples - this is fine, as the context # (i.e., immediate exception) is appended. interesting_examples = [ self._runner.interesting_examples[origin] for origin in err._interesting_origins if origin in self._runner.interesting_examples ] exceptions = [result.expected_exception for result in interesting_examples] exceptions.append(context) # the immediate exception return FlakyFailure(err.reason, exceptions) def _execute_once_for_engine(self, data: ConjectureData) -> None: """Wrapper around ``execute_once`` that intercepts test failure exceptions and single-test control exceptions, and turns them into appropriate method calls to `data` instead. This allows the engine to assume that any exception other than ``StopTest`` must be a fatal error, and should stop the entire engine. """ trace: Trace = frozenset() try: with Tracer(should_trace=self._should_trace()) as tracer: try: result = self.execute_once(data) if ( data.status == Status.VALID and tracer.branches ): # pragma: no cover # This is in fact covered by our *non-coverage* tests, but due # to the settrace() contention *not* by our coverage tests. self.explain_traces[None].add(tracer.branches) finally: trace = tracer.branches if result is not None: fail_health_check( self.settings, "Tests run under @given should return None, but " f"{self.test.__name__} returned {result!r} instead.", HealthCheck.return_value, ) except UnsatisfiedAssumption as e: # An "assume" check failed, so instead we inform the engine that # this test run was invalid. try: data.mark_invalid(e.reason) except FlakyReplay as err: # This was unexpected, meaning that the assume was flaky. # Report it as such. raise self._flaky_replay_to_failure(err, e) from None except (StopTest, BackendCannotProceed): # The engine knows how to handle this control exception, so it's # OK to re-raise it. raise except ( FailedHealthCheck, *skip_exceptions_to_reraise(), ): # These are fatal errors or control exceptions that should stop the # engine, so we re-raise them. raise except failure_exceptions_to_catch() as e: # If an unhandled (i.e., non-Hypothesis) error was raised by # Hypothesis-internal code, re-raise it as a fatal error instead # of treating it as a test failure. if isinstance(e, BaseExceptionGroup) and len(e.exceptions) == 1: # When a naked exception is implicitly wrapped in an ExceptionGroup # due to a re-raising "except*", the ExceptionGroup is constructed in # the caller's stack frame (see #4183). This workaround is specifically # for implicit wrapping of naked exceptions by "except*", since explicit # raising of ExceptionGroup gets the proper traceback in the first place # - there's no need to handle hierarchical groups here, at least if no # such implicit wrapping happens inside hypothesis code (we only care # about the hypothesis-or-not distinction). # # 01-25-2025: this was patched to give the correct # stacktrace in cpython https://github.com/python/cpython/issues/128799. # can remove once python3.11 is EOL. tb = e.exceptions[0].__traceback__ or e.__traceback__ else: tb = e.__traceback__ filepath = traceback.extract_tb(tb)[-1][0] if ( is_hypothesis_file(filepath) and not isinstance(e, HypothesisException) # We expect backend authors to use the provider_conformance test # to test their backends. If an error occurs there, it is probably # from their backend, and we would like to treat it as a standard # error, not a hypothesis-internal error. and not filepath.endswith( f"internal{os.sep}conjecture{os.sep}provider_conformance.py" ) ): raise if data.frozen: # This can happen if an error occurred in a finally # block somewhere, suppressing our original StopTest. # We raise a new one here to resume normal operation. raise StopTest(data.testcounter) from e else: # The test failed by raising an exception, so we inform the # engine that this test run was interesting. This is the normal # path for test runs that fail. tb = get_trimmed_traceback() data.expected_traceback = format_exception(e, tb) data.expected_exception = e assert data.expected_traceback is not None # for mypy verbose_report(data.expected_traceback) self.failed_normally = True interesting_origin = InterestingOrigin.from_exception(e) if trace: # pragma: no cover # Trace collection is explicitly disabled under coverage. self.explain_traces[interesting_origin].add(trace) if interesting_origin.exc_type == DeadlineExceeded: self.failed_due_to_deadline = True self.explain_traces.clear() try: data.mark_interesting(interesting_origin) except FlakyReplay as err: raise self._flaky_replay_to_failure(err, e) from None finally: # Conditional here so we can save some time constructing the payload; in # other cases (without coverage) it's cheap enough to do that regardless. # # Note that we have to unconditionally realize data.events, because # the statistics reported by the pytest plugin use a different flow # than observability, but still access symbolic events. try: data.events = data.provider.realize(data.events) except BackendCannotProceed: data.events = {} if observability_enabled(): if runner := getattr(self, "_runner", None): phase = runner._current_phase else: # pragma: no cover # in case of messing with internals if self.failed_normally or self.failed_due_to_deadline: phase = "shrink" else: phase = "unknown" backend_desc = f", using backend={self.settings.backend!r}" * ( self.settings.backend != "hypothesis" and not getattr(runner, "_switch_to_hypothesis_provider", False) ) try: data._observability_args = data.provider.realize( data._observability_args ) except BackendCannotProceed: data._observability_args = {} try: self._string_repr = data.provider.realize(self._string_repr) except BackendCannotProceed: self._string_repr = "" data.freeze() tc = make_testcase( run_start=self._start_timestamp, property=self.test_identifier, data=data, how_generated=f"during {phase} phase{backend_desc}", representation=self._string_repr, arguments=data._observability_args, timing=self._timing_features, coverage=tractable_coverage_report(trace) or None, phase=phase, backend_metadata=data.provider.observe_test_case(), ) deliver_observation(tc) for msg in data.provider.observe_information_messages( lifetime="test_case" ): self._deliver_information_message(**msg) self._timing_features = {} def _deliver_information_message( self, *, type: InfoObservationType, title: str, content: str | dict ) -> None: deliver_observation( InfoObservation( type=type, run_start=self._start_timestamp, property=self.test_identifier, title=title, content=content, ) ) def run_engine(self): """Run the test function many times, on database input and generated input, using the Conjecture engine. """ # Tell pytest to omit the body of this function from tracebacks __tracebackhide__ = True try: database_key = self.wrapped_test._hypothesis_internal_database_key except AttributeError: if global_force_seed is None: database_key = function_digest(self.test) else: database_key = None runner = ConjectureRunner( self._execute_once_for_engine, settings=self.settings, random=self.random, database_key=database_key, thread_overlap=self.thread_overlap, ) self._runner = runner # Use the Conjecture engine to run the test function many times # on different inputs. runner.run() note_statistics(runner.statistics) if observability_enabled(): self._deliver_information_message( type="info", title="Hypothesis Statistics", content=describe_statistics(runner.statistics), ) for msg in ( p if isinstance(p := runner.provider, PrimitiveProvider) else p(None) ).observe_information_messages(lifetime="test_function"): self._deliver_information_message(**msg) if runner.call_count == 0: return if runner.interesting_examples: self.falsifying_examples = sorted( runner.interesting_examples.values(), key=lambda d: sort_key(d.nodes), reverse=True, ) else: if runner.valid_examples == 0: explanations = [] # use a somewhat arbitrary cutoff to avoid recommending spurious # fixes. # eg, a few invalid examples from internal filters when the # problem is the user generating large inputs, or a # few overruns during internal mutation when the problem is # impossible user filters/assumes. if runner.invalid_examples > min(20, runner.call_count // 5): explanations.append( f"{runner.invalid_examples} of {runner.call_count} " "examples failed a .filter() or assume() condition. Try " "making your filters or assumes less strict, or rewrite " "using strategy parameters: " "st.integers().filter(lambda x: x > 0) fails less often " "(that is, never) when rewritten as st.integers(min_value=1)." ) if runner.overrun_examples > min(20, runner.call_count // 5): explanations.append( f"{runner.overrun_examples} of {runner.call_count} " "examples were too large to finish generating; try " "reducing the typical size of your inputs?" ) rep = get_pretty_function_description(self.test) raise Unsatisfiable( f"Unable to satisfy assumptions of {rep}. " f"{' Also, '.join(explanations)}" ) # If we have not traced executions, warn about that now (but only when # we'd expect to do so reliably, i.e. on CPython>=3.12) if ( hasattr(sys, "monitoring") and not PYPY and self._should_trace() and not Tracer.can_trace() ): # pragma: no cover # actually covered by our tests, but only on >= 3.12 warnings.warn( "avoiding tracing test function because tool id " f"{MONITORING_TOOL_ID} is already taken by tool " f"{sys.monitoring.get_tool(MONITORING_TOOL_ID)}.", HypothesisWarning, stacklevel=3, ) if not self.falsifying_examples: return elif not (self.settings.report_multiple_bugs and pytest_shows_exceptiongroups): # Pretend that we only found one failure, by discarding the others. del self.falsifying_examples[:-1] # The engine found one or more failures, so we need to reproduce and # report them. errors_to_report = [] report_lines = describe_targets(runner.best_observed_targets) if report_lines: report_lines.append("") explanations = explanatory_lines(self.explain_traces, self.settings) for falsifying_example in self.falsifying_examples: fragments = [] ran_example = runner.new_conjecture_data( falsifying_example.choices, max_choices=len(falsifying_example.choices) ) ran_example.slice_comments = falsifying_example.slice_comments tb = None origin = None assert falsifying_example.expected_exception is not None assert falsifying_example.expected_traceback is not None try: with with_reporter(fragments.append): self.execute_once( ran_example, print_example=True, is_final=True, expected_failure=( falsifying_example.expected_exception, falsifying_example.expected_traceback, ), ) except StopTest as e: # Link the expected exception from the first run. Not sure # how to access the current exception, if it failed # differently on this run. In fact, in the only known # reproducer, the StopTest is caused by OVERRUN before the # test is even executed. Possibly because all initial examples # failed until the final non-traced replay, and something was # exhausted? Possibly a FIXME, but sufficiently weird to # ignore for now. err = FlakyFailure( "Inconsistent results: An example failed on the " "first run but now succeeds (or fails with another " "error, or is for some reason not runnable).", # (note: e is a BaseException) [falsifying_example.expected_exception or e], ) errors_to_report.append(ReportableError(fragments, err)) except UnsatisfiedAssumption as e: # pragma: no cover # ironically flaky err = FlakyFailure( "Unreliable assumption: An example which satisfied " "assumptions on the first run now fails it.", [e], ) errors_to_report.append(ReportableError(fragments, err)) except BaseException as e: # If we have anything for explain-mode, this is the time to report. fragments.extend(explanations[falsifying_example.interesting_origin]) error_with_tb = e.with_traceback(get_trimmed_traceback()) errors_to_report.append(ReportableError(fragments, error_with_tb)) tb = format_exception(e, get_trimmed_traceback(e)) origin = InterestingOrigin.from_exception(e) else: # execute_once() will always raise either the expected error, or Flaky. raise NotImplementedError("This should be unreachable") finally: ran_example.freeze() if observability_enabled(): # log our observability line for the final failing example tc = make_testcase( run_start=self._start_timestamp, property=self.test_identifier, data=ran_example, how_generated="minimal failing example", representation=self._string_repr, arguments=ran_example._observability_args, timing=self._timing_features, coverage=None, # Not recorded when we're replaying the MFE status="passed" if sys.exc_info()[0] else "failed", status_reason=str(origin or "unexpected/flaky pass"), metadata={"traceback": tb}, ) deliver_observation(tc) # Whether or not replay actually raised the exception again, we want # to print the reproduce_failure decorator for the failing example. if self.settings.print_blob: fragments.append( "\nYou can reproduce this example by temporarily adding " f"{reproduction_decorator(falsifying_example.choices)} " "as a decorator on your test case" ) _raise_to_user( errors_to_report, self.settings, report_lines, # A backend might report a failure and then report verified afterwards, # which is to be interpreted as "there are no more failures *other # than what we already reported*". Do not report this as unsound. unsound_backend=( runner._verified_by_backend if runner._verified_by_backend and not runner._backend_found_failure else None ), ) def _simplify_explicit_errors(errors: list[ReportableError]) -> list[ReportableError]: """ Group explicit example errors by their InterestingOrigin, keeping only the simplest one, and adding a note of how many other examples failed with the same error. """ by_origin: dict[InterestingOrigin, list[ReportableError]] = defaultdict(list) for error in errors: origin = InterestingOrigin.from_exception(error.exception) by_origin[origin].append(error) result = [] for group in by_origin.values(): if len(group) == 1: result.append(group[0]) else: # Sort by shortlex of representation (first fragment) def shortlex_key(error): repr_str = error.fragments[0] if error.fragments else "" return (len(repr_str), repr_str) sorted_group = sorted(group, key=shortlex_key) simplest = sorted_group[0] other_count = len(group) - 1 add_note( simplest.exception, f"(note: {other_count} other explicit example{'s' * (other_count > 1)} " "also failed with this error; use Verbosity.verbose to view)", ) result.append(simplest) return result def _raise_to_user( errors_to_report, settings, target_lines, trailer="", *, unsound_backend=None ): """Helper function for attaching notes and grouping multiple errors.""" failing_prefix = "Falsifying example: " ls = [] for error in errors_to_report: for note in error.fragments: add_note(error.exception, note) if note.startswith(failing_prefix): ls.append(note.removeprefix(failing_prefix)) if current_pytest_item.value: current_pytest_item.value._hypothesis_failing_examples = ls if len(errors_to_report) == 1: the_error_hypothesis_found = errors_to_report[0].exception else: assert errors_to_report the_error_hypothesis_found = BaseExceptionGroup( f"Hypothesis found {len(errors_to_report)} distinct failures{trailer}.", [error.exception for error in errors_to_report], ) if settings.verbosity >= Verbosity.normal: for line in target_lines: add_note(the_error_hypothesis_found, line) if unsound_backend: add_note( the_error_hypothesis_found, f"backend={unsound_backend!r} claimed to verify this test passes - " "please send them a bug report!", ) raise the_error_hypothesis_found @contextlib.contextmanager def fake_subTest(self, msg=None, **__): """Monkeypatch for `unittest.TestCase.subTest` during `@given`. If we don't patch this out, each failing example is reported as a separate failing test by the unittest test runner, which is obviously incorrect. We therefore replace it for the duration with this version. """ warnings.warn( "subTest per-example reporting interacts badly with Hypothesis " "trying hundreds of examples, so we disable it for the duration of " "any test that uses `@given`.", HypothesisWarning, stacklevel=2, ) yield @dataclass(slots=False, frozen=False) class HypothesisHandle: """This object is provided as the .hypothesis attribute on @given tests. Downstream users can reassign its attributes to insert custom logic into the execution of each case, for example by converting an async into a sync function. This must be an attribute of an attribute, because reassignment of a first-level attribute would not be visible to Hypothesis if the function had been decorated before the assignment. See https://github.com/HypothesisWorks/hypothesis/issues/1257 for more information. """ inner_test: Any _get_fuzz_target: Any _given_kwargs: Any @property def fuzz_one_input( self, ) -> Callable[[bytes | bytearray | memoryview | BinaryIO], bytes | None]: """ Run the test as a fuzz target, driven with the ``buffer`` of bytes. Depending on the passed ``buffer`` one of three things will happen: * If the bytestring was invalid, for example because it was too short or was filtered out by |assume| or |.filter|, |fuzz_one_input| returns ``None``. * If the bytestring was valid and the test passed, |fuzz_one_input| returns a canonicalised and pruned bytestring which will replay that test case. This is provided as an option to improve the performance of mutating fuzzers, but can safely be ignored. * If the test *failed*, i.e. raised an exception, |fuzz_one_input| will add the pruned buffer to :ref:`the Hypothesis example database ` and then re-raise that exception. All you need to do to reproduce, minimize, and de-duplicate all the failures found via fuzzing is run your test suite! To reduce the performance impact of database writes, |fuzz_one_input| only records failing inputs which would be valid shrinks for a known failure - meaning writes are somewhere between constant and log(N) rather than linear in runtime. However, this tracking only works within a persistent fuzzing process; for forkserver fuzzers we recommend ``database=None`` for the main run, and then replaying with a database enabled if you need to analyse failures. Note that the interpretation of both input and output bytestrings is specific to the exact version of Hypothesis you are using and the strategies given to the test, just like the :ref:`database ` and |@reproduce_failure|. Interaction with |@settings| ---------------------------- |fuzz_one_input| uses just enough of Hypothesis' internals to drive your test function with a bytestring, and most settings therefore have no effect in this mode. We recommend running your tests the usual way before fuzzing to get the benefits of health checks, as well as afterwards to replay, shrink, deduplicate, and report whatever errors were discovered. * |settings.database| *is* used by |fuzz_one_input| - adding failures to the database to be replayed when you next run your tests is our preferred reporting mechanism and response to `the 'fuzzer taming' problem `__. * |settings.verbosity| and |settings.stateful_step_count| work as usual. * The |~settings.deadline|, |~settings.derandomize|, |~settings.max_examples|, |~settings.phases|, |~settings.print_blob|, |~settings.report_multiple_bugs|, and |~settings.suppress_health_check| settings do not affect |fuzz_one_input|. Example Usage ------------- .. code-block:: python @given(st.text()) def test_foo(s): ... # This is a traditional fuzz target - call it with a bytestring, # or a binary IO object, and it runs the test once. fuzz_target = test_foo.hypothesis.fuzz_one_input # For example: fuzz_target(b"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00") fuzz_target(io.BytesIO(b"\\x01")) .. tip:: If you expect to discover many failures while using |fuzz_one_input|, consider wrapping your database with |BackgroundWriteDatabase|, for low-overhead writes of failures. .. tip:: | Want an integrated workflow for your team's local tests, CI, and continuous fuzzing? | Use `HypoFuzz `__ to fuzz your whole test suite, and find more bugs with the same tests! .. seealso:: See also the :doc:`/how-to/external-fuzzers` how-to. """ # Note: most users, if they care about fuzzer performance, will access the # property and assign it to a local variable to move the attribute lookup # outside their fuzzing loop / before the fork point. We cache it anyway, # so that naive or unusual use-cases get the best possible performance too. try: return self.__cached_target # type: ignore except AttributeError: self.__cached_target = self._get_fuzz_target() return self.__cached_target @overload def given( _: EllipsisType, / ) -> Callable[ [Callable[..., Coroutine[Any, Any, None] | None]], Callable[[], None] ]: # pragma: no cover ... @overload def given( *_given_arguments: SearchStrategy[Any], ) -> Callable[ [Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None] ]: # pragma: no cover ... @overload def given( **_given_kwargs: SearchStrategy[Any] | EllipsisType, ) -> Callable[ [Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None] ]: # pragma: no cover ... def given( *_given_arguments: SearchStrategy[Any] | EllipsisType, **_given_kwargs: SearchStrategy[Any] | EllipsisType, ) -> Callable[[Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None]]: """ The |@given| decorator turns a function into a Hypothesis test. This is the main entry point to Hypothesis. .. seealso:: See also the :doc:`/tutorial/introduction` tutorial, which introduces defining Hypothesis tests with |@given|. .. _given-arguments: Arguments to ``@given`` ----------------------- Arguments to |@given| may be either positional or keyword arguments: .. code-block:: python @given(st.integers(), st.floats()) def test_one(x, y): pass @given(x=st.integers(), y=st.floats()) def test_two(x, y): pass If using keyword arguments, the arguments may appear in any order, as with standard Python functions: .. code-block:: python # different order, but still equivalent to before @given(y=st.floats(), x=st.integers()) def test(x, y): assert isinstance(x, int) assert isinstance(y, float) If |@given| is provided fewer positional arguments than the decorated test, the test arguments are filled in on the right side, leaving the leftmost positional arguments unfilled: .. code-block:: python @given(st.integers(), st.floats()) def test(manual_string, y, z): assert manual_string == "x" assert isinstance(y, int) assert isinstance(z, float) # `test` is now a callable which takes one argument `manual_string` test("x") # or equivalently: test(manual_string="x") The reason for this "from the right" behavior is to support using |@given| with instance methods, by automatically passing through ``self``: .. code-block:: python class MyTest(TestCase): @given(st.integers()) def test(self, x): assert isinstance(self, MyTest) assert isinstance(x, int) If (and only if) using keyword arguments, |@given| may be combined with ``**kwargs`` or ``*args``: .. code-block:: python @given(x=integers(), y=integers()) def test(x, **kwargs): assert "y" in kwargs @given(x=integers(), y=integers()) def test(x, *args, **kwargs): assert args == () assert "x" not in kwargs assert "y" in kwargs It is an error to: * Mix positional and keyword arguments to |@given|. * Use |@given| with a function that has a default value for an argument. * Use |@given| with positional arguments with a function that uses ``*args``, ``**kwargs``, or keyword-only arguments. The function returned by given has all the same arguments as the original test, minus those that are filled in by |@given|. See the :ref:`notes on framework compatibility ` for how this interacts with features of other testing libraries, such as :pypi:`pytest` fixtures. """ if currently_in_test_context(): fail_health_check( Settings(), "Nesting @given tests results in quadratic generation and shrinking " "behavior, and can usually be more cleanly expressed by replacing the " "inner function with an st.data() parameter on the outer @given." "\n\n" "If it is difficult or impossible to refactor this test to remove the " "nested @given, you can disable this health check with " "@settings(suppress_health_check=[HealthCheck.nested_given]) on the " "outer @given. See " "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " "for details.", HealthCheck.nested_given, ) def run_test_as_given(test): if inspect.isclass(test): # Provide a meaningful error to users, instead of exceptions from # internals that assume we're dealing with a function. raise InvalidArgument("@given cannot be applied to a class") if ( "_pytest" in sys.modules and "_pytest.fixtures" in sys.modules and ( tuple(map(int, sys.modules["_pytest"].__version__.split(".")[:2])) >= (8, 4) ) and isinstance( test, sys.modules["_pytest.fixtures"].FixtureFunctionDefinition ) ): # pragma: no cover # covered by pytest/test_fixtures, but not by cover/ raise InvalidArgument("@given cannot be applied to a pytest fixture") given_arguments = tuple(_given_arguments) given_kwargs = dict(_given_kwargs) original_sig = get_signature(test) if given_arguments == (Ellipsis,) and not given_kwargs: # user indicated that they want to infer all arguments given_kwargs = { p.name: Ellipsis for p in original_sig.parameters.values() if p.kind in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY) } given_arguments = () check_invalid = is_invalid_test( test, original_sig, given_arguments, given_kwargs ) # If the argument check found problems, return a dummy test function # that will raise an error if it is actually called. if check_invalid is not None: return check_invalid # Because the argument check succeeded, we can convert @given's # positional arguments into keyword arguments for simplicity. if given_arguments: assert not given_kwargs posargs = [ p.name for p in original_sig.parameters.values() if p.kind is p.POSITIONAL_OR_KEYWORD ] given_kwargs = dict( list(zip(posargs[::-1], given_arguments[::-1], strict=False))[::-1] ) # These have been converted, so delete them to prevent accidental use. del given_arguments new_signature = new_given_signature(original_sig, given_kwargs) # Use type information to convert "infer" arguments into appropriate strategies. if ... in given_kwargs.values(): hints = get_type_hints(test) for name in [name for name, value in given_kwargs.items() if value is ...]: if name not in hints: return _invalid( f"passed {name}=... for {test.__name__}, but {name} has " "no type annotation", test=test, given_kwargs=given_kwargs, ) given_kwargs[name] = st.from_type(hints[name]) # only raise if the same thread uses two different executors, not if two # different threads use different executors. thread_local = ThreadLocal(prev_self=lambda: not_set) # maps thread_id to whether that thread overlaps in execution with any # other thread in this @given. We use this to detect whether an @given is # being run from multiple different threads at once, which informs # decisions like whether to raise DeadlineExceeded or HealthCheck.too_slow. thread_overlap: dict[int, bool] = {} thread_overlap_lock = Lock() @impersonate(test) @define_function_signature(test.__name__, test.__doc__, new_signature) def wrapped_test(*arguments, **kwargs): # Tell pytest to omit the body of this function from tracebacks __tracebackhide__ = True with thread_overlap_lock: for overlap_thread_id in thread_overlap: thread_overlap[overlap_thread_id] = True threadid = threading.get_ident() # if there are existing threads when this thread starts, then # this thread starts at an overlapped state. has_existing_threads = len(thread_overlap) > 0 thread_overlap[threadid] = has_existing_threads try: test = wrapped_test.hypothesis.inner_test if getattr(test, "is_hypothesis_test", False): raise InvalidArgument( f"You have applied @given to the test {test.__name__} more than " "once, which wraps the test several times and is extremely slow. " "A similar effect can be gained by combining the arguments " "of the two calls to given. For example, instead of " "@given(booleans()) @given(integers()), you could write " "@given(booleans(), integers())" ) settings = wrapped_test._hypothesis_internal_use_settings random = get_random_for_wrapped_test(test, wrapped_test) arguments, kwargs, stuff = process_arguments_to_given( wrapped_test, arguments, kwargs, given_kwargs, new_signature.parameters, ) if ( inspect.iscoroutinefunction(test) and get_executor(stuff.selfy) is default_executor ): # See https://github.com/HypothesisWorks/hypothesis/issues/3054 # If our custom executor doesn't handle coroutines, or we return an # awaitable from a non-async-def function, we just rely on the # return_value health check. This catches most user errors though. raise InvalidArgument( "Hypothesis doesn't know how to run async test functions like " f"{test.__name__}. You'll need to write a custom executor, " "or use a library like pytest-asyncio or pytest-trio which can " "handle the translation for you.\n See https://hypothesis." "readthedocs.io/en/latest/details.html#custom-function-execution" ) runner = stuff.selfy if isinstance(stuff.selfy, TestCase) and test.__name__ in dir(TestCase): fail_health_check( settings, f"You have applied @given to the method {test.__name__}, which is " "used by the unittest runner but is not itself a test. " "This is not useful in any way.", HealthCheck.not_a_test_method, ) if bad_django_TestCase(runner): # pragma: no cover # Covered by the Django tests, but not the pytest coverage task raise InvalidArgument( "You have applied @given to a method on " f"{type(runner).__qualname__}, but this " "class does not inherit from the supported versions in " "`hypothesis.extra.django`. Use the Hypothesis variants " "to ensure that each example is run in a separate " "database transaction." ) nonlocal thread_local # Check selfy really is self (not e.g. a mock) before we health-check cur_self = ( stuff.selfy if getattr(type(stuff.selfy), test.__name__, None) is wrapped_test else None ) if thread_local.prev_self is not_set: thread_local.prev_self = cur_self elif cur_self is not thread_local.prev_self: fail_health_check( settings, f"The method {test.__qualname__} was called from multiple " "different executors. This may lead to flaky tests and " "nonreproducible errors when replaying from database." "\n\n" "Unlike most health checks, HealthCheck.differing_executors " "warns about a correctness issue with your test. We " "therefore recommend fixing the underlying issue, rather " "than suppressing this health check. However, if you are " "confident this health check can be safely disabled, you can " "do so with " "@settings(suppress_health_check=[HealthCheck.differing_executors]). " "See " "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " "for details.", HealthCheck.differing_executors, ) state = StateForActualGivenExecution( stuff, test, settings, random, wrapped_test, thread_overlap=thread_overlap, ) # If there was a @reproduce_failure decorator, use it to reproduce # the error (or complain that we couldn't). Either way, this will # always raise some kind of error. if ( reproduce_failure := wrapped_test._hypothesis_internal_use_reproduce_failure ) is not None: expected_version, failure = reproduce_failure if expected_version != __version__: raise InvalidArgument( "Attempting to reproduce a failure from a different " f"version of Hypothesis. This failure is from {expected_version}, but " f"you are currently running {__version__!r}. Please change your " "Hypothesis version to a matching one." ) try: state.execute_once( ConjectureData.for_choices(decode_failure(failure)), print_example=True, is_final=True, ) raise DidNotReproduce( "Expected the test to raise an error, but it " "completed successfully." ) except StopTest: raise DidNotReproduce( "The shape of the test data has changed in some way " "from where this blob was defined. Are you sure " "you're running the same test?" ) from None except UnsatisfiedAssumption: raise DidNotReproduce( "The test data failed to satisfy an assumption in the " "test. Have you added it since this blob was generated?" ) from None # There was no @reproduce_failure, so start by running any explicit # examples from @example decorators. if errors := list( execute_explicit_examples( state, wrapped_test, arguments, kwargs, original_sig ) ): # If we're not going to report multiple bugs, we would have # stopped running explicit examples at the first failure. assert len(errors) == 1 or state.settings.report_multiple_bugs # If an explicit example raised a 'skip' exception, ensure it's never # wrapped up in an exception group. Because we break out of the loop # immediately on finding a skip, if present it's always the last error. if isinstance(errors[-1].exception, skip_exceptions_to_reraise()): # Covered by `test_issue_3453_regression`, just in a subprocess. del errors[:-1] # pragma: no cover if state.settings.verbosity < Verbosity.verbose: # keep only one error per interesting origin, unless # verbosity is high errors = _simplify_explicit_errors(errors) _raise_to_user(errors, state.settings, [], " in explicit examples") # If there were any explicit examples, they all ran successfully. # The next step is to use the Conjecture engine to run the test on # many different inputs. ran_explicit_examples = ( Phase.explicit in state.settings.phases and getattr(wrapped_test, "hypothesis_explicit_examples", ()) ) SKIP_BECAUSE_NO_EXAMPLES = unittest.SkipTest( "Hypothesis has been told to run no examples for this test." ) if not ( Phase.reuse in settings.phases or Phase.generate in settings.phases ): if not ran_explicit_examples: raise SKIP_BECAUSE_NO_EXAMPLES return try: if isinstance(runner, TestCase) and hasattr(runner, "subTest"): subTest = runner.subTest try: runner.subTest = types.MethodType(fake_subTest, runner) state.run_engine() finally: runner.subTest = subTest else: state.run_engine() except BaseException as e: # The exception caught here should either be an actual test # failure (or BaseExceptionGroup), or some kind of fatal error # that caused the engine to stop. generated_seed = ( wrapped_test._hypothesis_internal_use_generated_seed ) with local_settings(settings): if not (state.failed_normally or generated_seed is None): if running_under_pytest: report( f"You can add @seed({generated_seed}) to this test or " f"run pytest with --hypothesis-seed={generated_seed} " "to reproduce this failure." ) else: report( f"You can add @seed({generated_seed}) to this test to " "reproduce this failure." ) # The dance here is to avoid showing users long tracebacks # full of Hypothesis internals they don't care about. # We have to do this inline, to avoid adding another # internal stack frame just when we've removed the rest. # # Using a variable for our trimmed error ensures that the line # which will actually appear in tracebacks is as clear as # possible - "raise the_error_hypothesis_found". the_error_hypothesis_found = e.with_traceback( None if isinstance(e, BaseExceptionGroup) else get_trimmed_traceback() ) raise the_error_hypothesis_found if not (ran_explicit_examples or state.ever_executed): raise SKIP_BECAUSE_NO_EXAMPLES finally: with thread_overlap_lock: del thread_overlap[threadid] def _get_fuzz_target() -> ( Callable[[bytes | bytearray | memoryview | BinaryIO], bytes | None] ): # Because fuzzing interfaces are very performance-sensitive, we use a # somewhat more complicated structure here. `_get_fuzz_target()` is # called by the `HypothesisHandle.fuzz_one_input` property, allowing # us to defer our collection of the settings, random instance, and # reassignable `inner_test` (etc) until `fuzz_one_input` is accessed. # # We then share the performance cost of setting up `state` between # many invocations of the target. We explicitly force `deadline=None` # for performance reasons, saving ~40% the runtime of an empty test. test = wrapped_test.hypothesis.inner_test settings = Settings( parent=wrapped_test._hypothesis_internal_use_settings, deadline=None ) random = get_random_for_wrapped_test(test, wrapped_test) _args, _kwargs, stuff = process_arguments_to_given( wrapped_test, (), {}, given_kwargs, new_signature.parameters ) assert not _args assert not _kwargs state = StateForActualGivenExecution( stuff, test, settings, random, wrapped_test, thread_overlap=thread_overlap, ) database_key = function_digest(test) + b".secondary" # We track the minimal-so-far example for each distinct origin, so # that we track log-n instead of n examples for long runs. In particular # it means that we saturate for common errors in long runs instead of # storing huge volumes of low-value data. minimal_failures: dict = {} def fuzz_one_input( buffer: bytes | bytearray | memoryview | BinaryIO, ) -> bytes | None: # This inner part is all that the fuzzer will actually run, # so we keep it as small and as fast as possible. if isinstance(buffer, io.IOBase): buffer = buffer.read(BUFFER_SIZE) assert isinstance(buffer, (bytes, bytearray, memoryview)) data = ConjectureData( random=None, provider=BytestringProvider, provider_kw={"bytestring": buffer}, ) try: state.execute_once(data) status = Status.VALID except StopTest: status = data.status return None except UnsatisfiedAssumption: status = Status.INVALID return None except BaseException: known = minimal_failures.get(data.interesting_origin) if settings.database is not None and ( known is None or sort_key(data.nodes) <= sort_key(known) ): settings.database.save( database_key, choices_to_bytes(data.choices) ) minimal_failures[data.interesting_origin] = data.nodes status = Status.INTERESTING raise finally: if observability_enabled(): data.freeze() tc = make_testcase( run_start=state._start_timestamp, property=state.test_identifier, data=data, how_generated="fuzz_one_input", representation=state._string_repr, arguments=data._observability_args, timing=state._timing_features, coverage=None, status=status, backend_metadata=data.provider.observe_test_case(), ) deliver_observation(tc) state._timing_features = {} assert isinstance(data.provider, BytestringProvider) return bytes(data.provider.drawn) fuzz_one_input.__doc__ = HypothesisHandle.fuzz_one_input.__doc__ return fuzz_one_input # After having created the decorated test function, we need to copy # over some attributes to make the switch as seamless as possible. for attrib in dir(test): if not (attrib.startswith("_") or hasattr(wrapped_test, attrib)): setattr(wrapped_test, attrib, getattr(test, attrib)) wrapped_test.is_hypothesis_test = True if hasattr(test, "_hypothesis_internal_settings_applied"): # Used to check if @settings is applied twice. wrapped_test._hypothesis_internal_settings_applied = True wrapped_test._hypothesis_internal_use_seed = getattr( test, "_hypothesis_internal_use_seed", None ) wrapped_test._hypothesis_internal_use_settings = ( getattr(test, "_hypothesis_internal_use_settings", None) or Settings.default ) wrapped_test._hypothesis_internal_use_reproduce_failure = getattr( test, "_hypothesis_internal_use_reproduce_failure", None ) wrapped_test.hypothesis = HypothesisHandle(test, _get_fuzz_target, given_kwargs) return wrapped_test return run_test_as_given def find( specifier: SearchStrategy[Ex], condition: Callable[[Any], bool], *, settings: Settings | None = None, random: Random | None = None, database_key: bytes | None = None, ) -> Ex: """Returns the minimal example from the given strategy ``specifier`` that matches the predicate function ``condition``.""" if settings is None: settings = Settings(max_examples=2000) settings = Settings( settings, suppress_health_check=list(HealthCheck), report_multiple_bugs=False ) if database_key is None and settings.database is not None: # Note: The database key is not guaranteed to be unique. If not, replaying # of database examples may fail to reproduce due to being replayed on the # wrong condition. database_key = function_digest(condition) if not isinstance(specifier, SearchStrategy): raise InvalidArgument( f"Expected SearchStrategy but got {specifier!r} of " f"type {type(specifier).__name__}" ) specifier.validate() last: list[Ex] = [] @settings @given(specifier) def test(v): if condition(v): last[:] = [v] raise Found if random is not None: test = seed(random.getrandbits(64))(test) test._hypothesis_internal_database_key = database_key # type: ignore try: test() except Found: return last[0] raise NoSuchExample(get_pretty_function_description(condition)) ================================================ FILE: hypothesis-python/src/hypothesis/database.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import abc import errno import json import os import struct import sys import tempfile import warnings import weakref from collections.abc import Callable, Iterable from datetime import datetime, timedelta, timezone from functools import lru_cache from hashlib import sha384 from os import PathLike, getenv from pathlib import Path, PurePath from queue import Queue from threading import Thread from typing import ( TYPE_CHECKING, Any, ClassVar, Literal, TypeAlias, cast, ) from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen from zipfile import BadZipFile, ZipFile from hypothesis.configuration import storage_directory from hypothesis.errors import HypothesisException, HypothesisWarning from hypothesis.internal.conjecture.choice import ChoiceT from hypothesis.utils.conventions import UniqueIdentifier, not_set from hypothesis.utils.deprecation import note_deprecation __all__ = [ "DirectoryBasedExampleDatabase", "ExampleDatabase", "GitHubArtifactDatabase", "InMemoryExampleDatabase", "MultiplexedDatabase", "ReadOnlyDatabase", ] if TYPE_CHECKING: from watchdog.observers.api import BaseObserver StrPathT: TypeAlias = str | PathLike[str] SaveDataT: TypeAlias = tuple[bytes, bytes] # key, value DeleteDataT: TypeAlias = tuple[bytes, bytes | None] # key, value ListenerEventT: TypeAlias = ( tuple[Literal["save"], SaveDataT] | tuple[Literal["delete"], DeleteDataT] ) ListenerT: TypeAlias = Callable[[ListenerEventT], Any] def _usable_dir(path: StrPathT) -> bool: """ Returns True if the desired path can be used as database path because either the directory exists and can be used, or its root directory can be used and we can make the directory as needed. """ path = Path(path) try: while not path.exists(): # Loop terminates because the root dir ('/' on unix) always exists. path = path.parent return path.is_dir() and os.access(path, os.R_OK | os.W_OK | os.X_OK) except PermissionError: # pragma: no cover # path.exists() returns False on 3.14+ instead of raising. See # https://docs.python.org/3.14/library/pathlib.html#querying-file-type-and-status return False def _db_for_path( path: StrPathT | UniqueIdentifier | Literal[":memory:"] | None = None, ) -> "ExampleDatabase": if path is not_set: if os.getenv("HYPOTHESIS_DATABASE_FILE") is not None: # pragma: no cover raise HypothesisException( "The $HYPOTHESIS_DATABASE_FILE environment variable no longer has any " "effect. Configure your database location via a settings profile instead.\n" "https://hypothesis.readthedocs.io/en/latest/settings.html#settings-profiles" ) path = storage_directory("examples", intent_to_write=False) if not _usable_dir(path): # pragma: no cover warnings.warn( "The database setting is not configured, and the default " "location is unusable - falling back to an in-memory " f"database for this session. {path=}", HypothesisWarning, stacklevel=3, ) return InMemoryExampleDatabase() if path in (None, ":memory:"): return InMemoryExampleDatabase() path = cast(StrPathT, path) return DirectoryBasedExampleDatabase(path) class _EDMeta(abc.ABCMeta): def __call__(self, *args: Any, **kwargs: Any) -> "ExampleDatabase": if self is ExampleDatabase: note_deprecation( "Creating a database using the abstract ExampleDatabase() class " "is deprecated. Prefer using a concrete subclass, like " "InMemoryExampleDatabase() or DirectoryBasedExampleDatabase(path). " 'In particular, the special string ExampleDatabase(":memory:") ' "should be replaced by InMemoryExampleDatabase().", since="2025-04-07", has_codemod=False, ) return _db_for_path(*args, **kwargs) return super().__call__(*args, **kwargs) # This __call__ method is picked up by Sphinx as the signature of all ExampleDatabase # subclasses, which is accurate, reasonable, and unhelpful. Fortunately Sphinx # maintains a list of metaclass-call-methods to ignore, and while they would prefer # not to maintain it upstream (https://github.com/sphinx-doc/sphinx/pull/8262) we # can insert ourselves here. # # This code only runs if Sphinx has already been imported; and it would live in our # docs/conf.py except that we would also like it to work for anyone documenting # downstream ExampleDatabase subclasses too. # # We avoid type-checking this block due to this combination facts: # * our check-types-api CI job runs under 3.14 # * tools.txt therefore pins to a newer version of sphinx which uses 3.12+ `type` # syntax # * in test_mypy.py, mypy sees this block, sees sphinx is installed, tries parsing # sphinx code, and errors # # Putting `and not TYPE_CHECKING` here is just a convenience for our testing setup # (because we don't split mypy tests by running CI version, eg), not for runtime # behavior. if "sphinx" in sys.modules and not TYPE_CHECKING: # pragma: no cover try: import sphinx.ext.autodoc signature = "hypothesis.database._EDMeta.__call__" # _METACLASS_CALL_BLACKLIST moved in newer sphinx versions try: import sphinx.ext.autodoc._dynamic._signatures as _module except ImportError: _module = sphinx.ext.autodoc # _METACLASS_CALL_BLACKLIST is a frozenset in later sphinx versions if isinstance(_module._METACLASS_CALL_BLACKLIST, frozenset): _module._METACLASS_CALL_BLACKLIST = _module._METACLASS_CALL_BLACKLIST | { signature } else: _module._METACLASS_CALL_BLACKLIST.append(signature) except Exception: pass class ExampleDatabase(metaclass=_EDMeta): """ A Hypothesis database, for use in |settings.database|. Hypothesis automatically saves failures to the database set in |settings.database|. The next time the test is run, Hypothesis will replay any failures from the database in |settings.database| for that test (in |Phase.reuse|). The database is best thought of as a cache that you never need to invalidate. Entries may be transparently dropped when upgrading your Hypothesis version or changing your test. Do not rely on the database for correctness; to ensure Hypothesis always tries an input, use |@example|. A Hypothesis database is a simple mapping of bytes to sets of bytes. Hypothesis provides several concrete database subclasses. To write your own database class, see :doc:`/how-to/custom-database`. Change listening ---------------- An optional extension to |ExampleDatabase| is change listening. On databases which support change listening, calling |ExampleDatabase.add_listener| adds a function as a change listener, which will be called whenever a value is added, deleted, or moved inside the database. See |ExampleDatabase.add_listener| for details. All databases in Hypothesis support change listening. Custom database classes are not required to support change listening, though they will not be compatible with features that require change listening until they do so. .. note:: While no Hypothesis features currently require change listening, change listening is required by `HypoFuzz `_. Database methods ---------------- Required methods: * |ExampleDatabase.save| * |ExampleDatabase.fetch| * |ExampleDatabase.delete| Optional methods: * |ExampleDatabase.move| Change listening methods: * |ExampleDatabase.add_listener| * |ExampleDatabase.remove_listener| * |ExampleDatabase.clear_listeners| * |ExampleDatabase._start_listening| * |ExampleDatabase._stop_listening| * |ExampleDatabase._broadcast_change| """ def __init__(self) -> None: self._listeners: list[ListenerT] = [] @abc.abstractmethod def save(self, key: bytes, value: bytes) -> None: """Save ``value`` under ``key``. If ``value`` is already present in ``key``, silently do nothing. """ raise NotImplementedError(f"{type(self).__name__}.save") @abc.abstractmethod def fetch(self, key: bytes) -> Iterable[bytes]: """Return an iterable over all values matching this key.""" raise NotImplementedError(f"{type(self).__name__}.fetch") @abc.abstractmethod def delete(self, key: bytes, value: bytes) -> None: """Remove ``value`` from ``key``. If ``value`` is not present in ``key``, silently do nothing. """ raise NotImplementedError(f"{type(self).__name__}.delete") def move(self, src: bytes, dest: bytes, value: bytes) -> None: """ Move ``value`` from key ``src`` to key ``dest``. Equivalent to ``delete(src, value)`` followed by ``save(src, value)``, but may have a more efficient implementation. Note that ``value`` will be inserted at ``dest`` regardless of whether it is currently present at ``src``. """ if src == dest: self.save(src, value) return self.delete(src, value) self.save(dest, value) def add_listener(self, f: ListenerT, /) -> None: """ Add a change listener. ``f`` will be called whenever a value is saved, deleted, or moved in the database. ``f`` can be called with two different event values: * ``("save", (key, value))`` * ``("delete", (key, value))`` where ``key`` and ``value`` are both ``bytes``. There is no ``move`` event. Instead, a move is broadcasted as a ``delete`` event followed by a ``save`` event. For the ``delete`` event, ``value`` may be ``None``. This might occur if the database knows that a deletion has occurred in ``key``, but does not know what value was deleted. """ had_listeners = bool(self._listeners) self._listeners.append(f) if not had_listeners: self._start_listening() def remove_listener(self, f: ListenerT, /) -> None: """ Removes ``f`` from the list of change listeners. If ``f`` is not in the list of change listeners, silently do nothing. """ if f not in self._listeners: return self._listeners.remove(f) if not self._listeners: self._stop_listening() def clear_listeners(self) -> None: """Remove all change listeners.""" had_listeners = bool(self._listeners) self._listeners.clear() if had_listeners: self._stop_listening() def _broadcast_change(self, event: ListenerEventT) -> None: """ Called when a value has been either added to or deleted from a key in the underlying database store. The possible values for ``event`` are: * ``("save", (key, value))`` * ``("delete", (key, value))`` ``value`` may be ``None`` for the ``delete`` event, indicating we know that some value was deleted under this key, but not its exact value. Note that you should not assume your instance is the only reference to the underlying database store. For example, if two instances of |DirectoryBasedExampleDatabase| reference the same directory, _broadcast_change should be called whenever a file is added or removed from the directory, even if that database was not responsible for changing the file. """ for listener in self._listeners: listener(event) def _start_listening(self) -> None: """ Called when the database adds a change listener, and did not previously have any change listeners. Intended to allow databases to wait to start expensive listening operations until necessary. ``_start_listening`` and ``_stop_listening`` are guaranteed to alternate, so you do not need to handle the case of multiple consecutive ``_start_listening`` calls without an intermediate ``_stop_listening`` call. """ warnings.warn( f"{self.__class__} does not support listening for changes", HypothesisWarning, stacklevel=4, ) def _stop_listening(self) -> None: """ Called whenever no change listeners remain on the database. ``_stop_listening`` and ``_start_listening`` are guaranteed to alternate, so you do not need to handle the case of multiple consecutive ``_stop_listening`` calls without an intermediate ``_start_listening`` call. """ warnings.warn( f"{self.__class__} does not support stopping listening for changes", HypothesisWarning, stacklevel=4, ) class InMemoryExampleDatabase(ExampleDatabase): """A non-persistent example database, implemented in terms of an in-memory dictionary. This can be useful if you call a test function several times in a single session, or for testing other database implementations, but because it does not persist between runs we do not recommend it for general use. """ def __init__(self) -> None: super().__init__() self.data: dict[bytes, set[bytes]] = {} def __repr__(self) -> str: return f"InMemoryExampleDatabase({self.data!r})" def __eq__(self, other: object) -> bool: return isinstance(other, InMemoryExampleDatabase) and self.data is other.data def fetch(self, key: bytes) -> Iterable[bytes]: yield from self.data.get(key, ()) def save(self, key: bytes, value: bytes) -> None: value = bytes(value) values = self.data.setdefault(key, set()) changed = value not in values values.add(value) if changed: self._broadcast_change(("save", (key, value))) def delete(self, key: bytes, value: bytes) -> None: value = bytes(value) values = self.data.get(key, set()) changed = value in values values.discard(value) if changed: self._broadcast_change(("delete", (key, value))) def _start_listening(self) -> None: # declare compatibility with the listener api, but do the actual # implementation in .delete and .save, since we know we are the only # writer to .data. pass def _stop_listening(self) -> None: pass def _hash(key: bytes) -> str: return sha384(key).hexdigest()[:16] class DirectoryBasedExampleDatabase(ExampleDatabase): """Use a directory to store Hypothesis examples as files. Each test corresponds to a directory, and each example to a file within that directory. While the contents are fairly opaque, a |DirectoryBasedExampleDatabase| can be shared by checking the directory into version control, for example with the following ``.gitignore``:: # Ignore files cached by Hypothesis... .hypothesis/* # except for the examples directory !.hypothesis/examples/ Note however that this only makes sense if you also pin to an exact version of Hypothesis, and we would usually recommend implementing a shared database with a network datastore - see |ExampleDatabase|, and the |MultiplexedDatabase| helper. """ # we keep a database entry of the full values of all the database keys. # currently only used for inverse mapping of hash -> key in change listening. _metakeys_name: ClassVar[bytes] = b".hypothesis-keys" _metakeys_hash: ClassVar[str] = _hash(_metakeys_name) def __init__(self, path: StrPathT) -> None: super().__init__() self.path = Path(path) self.keypaths: dict[bytes, Path] = {} self._observer: BaseObserver | None = None def __repr__(self) -> str: return f"DirectoryBasedExampleDatabase({self.path!r})" def __eq__(self, other: object) -> bool: return ( isinstance(other, DirectoryBasedExampleDatabase) and self.path == other.path ) def _key_path(self, key: bytes) -> Path: try: return self.keypaths[key] except KeyError: pass self.keypaths[key] = self.path / _hash(key) return self.keypaths[key] def _value_path(self, key: bytes, value: bytes) -> Path: return self._key_path(key) / _hash(value) def fetch(self, key: bytes) -> Iterable[bytes]: kp = self._key_path(key) if not kp.is_dir(): return try: for path in os.listdir(kp): try: yield (kp / path).read_bytes() except OSError: pass except OSError: # pragma: no cover # the `kp` directory might have been deleted in the meantime pass def save(self, key: bytes, value: bytes) -> None: key_path = self._key_path(key) if key_path.name != self._metakeys_hash: # add this key to our meta entry of all keys - taking care to avoid # infinite recursion. self.save(self._metakeys_name, key) # Note: we attempt to create the dir in question now. We # already checked for permissions, but there can still be other issues, # e.g. the disk is full, or permissions might have been changed. try: key_path.mkdir(exist_ok=True, parents=True) path = self._value_path(key, value) if not path.exists(): # to mimic an atomic write, create and write in a temporary # directory, and only move to the final path after. This avoids # any intermediate state where the file is created (and empty) # but not yet written to. fd, tmpname = tempfile.mkstemp() tmppath = Path(tmpname) os.write(fd, value) os.close(fd) try: tmppath.rename(path) except OSError as err: # pragma: no cover if err.errno == errno.EXDEV: # Can't rename across filesystem boundaries, see e.g. # https://github.com/HypothesisWorks/hypothesis/issues/4335 try: path.write_bytes(tmppath.read_bytes()) except OSError: pass tmppath.unlink() assert not tmppath.exists() except OSError: # pragma: no cover pass def move(self, src: bytes, dest: bytes, value: bytes) -> None: if src == dest: self.save(src, value) return src_path = self._value_path(src, value) dest_path = self._value_path(dest, value) # if the dest key path does not exist, os.renames will create it for us, # and we will never track its creation in the meta keys entry. Do so now. if not self._key_path(dest).exists(): self.save(self._metakeys_name, dest) try: os.renames(src_path, dest_path) except OSError: self.delete(src, value) self.save(dest, value) def delete(self, key: bytes, value: bytes) -> None: try: self._value_path(key, value).unlink() except OSError: return # try deleting the key dir, which will only succeed if the dir is empty # (i.e. ``value`` was the last value in this key). try: self._key_path(key).rmdir() except OSError: pass else: # if the deletion succeeded, also delete this key entry from metakeys. # (if this key happens to be the metakey itself, this deletion will # fail; that's ok and faster than checking for this rare case.) self.delete(self._metakeys_name, key) def _start_listening(self) -> None: try: from watchdog.events import ( DirCreatedEvent, DirDeletedEvent, DirMovedEvent, FileCreatedEvent, FileDeletedEvent, FileMovedEvent, FileSystemEventHandler, ) from watchdog.observers import Observer except ImportError: warnings.warn( f"listening for changes in a {self.__class__.__name__} " "requires the watchdog library. To install, run " "`pip install hypothesis[watchdog]`", HypothesisWarning, stacklevel=4, ) return hash_to_key = {_hash(key): key for key in self.fetch(self._metakeys_name)} _metakeys_hash = self._metakeys_hash _broadcast_change = self._broadcast_change class Handler( FileSystemEventHandler ): # pragma: no cover # skipped in test_database.py for now def on_created(_self, event: FileCreatedEvent | DirCreatedEvent) -> None: # we only registered for the file creation event assert not isinstance(event, DirCreatedEvent) # watchdog events are only bytes if we passed a byte path to # .schedule assert isinstance(event.src_path, str) value_path = Path(event.src_path) # the parent dir represents the key, and its name is the key hash key_hash = value_path.parent.name if key_hash == _metakeys_hash: try: hash_to_key[value_path.name] = value_path.read_bytes() except OSError: # pragma: no cover # this might occur if all the values in a key have been # deleted and DirectoryBasedExampleDatabase removes its # metakeys entry (which is `value_path` here`). pass return key = hash_to_key.get(key_hash) if key is None: # pragma: no cover # we didn't recognize this key. This shouldn't ever happen, # but some race condition trickery might cause this. return try: value = value_path.read_bytes() except OSError: # pragma: no cover return _broadcast_change(("save", (key, value))) def on_deleted(self, event: FileDeletedEvent | DirDeletedEvent) -> None: assert not isinstance(event, DirDeletedEvent) assert isinstance(event.src_path, str) value_path = Path(event.src_path) key = hash_to_key.get(value_path.parent.name) if key is None: # pragma: no cover return _broadcast_change(("delete", (key, None))) def on_moved(self, event: FileMovedEvent | DirMovedEvent) -> None: assert not isinstance(event, DirMovedEvent) assert isinstance(event.src_path, str) assert isinstance(event.dest_path, str) src_path = Path(event.src_path) dest_path = Path(event.dest_path) k1 = hash_to_key.get(src_path.parent.name) k2 = hash_to_key.get(dest_path.parent.name) if k1 is None or k2 is None: # pragma: no cover return try: value = dest_path.read_bytes() except OSError: # pragma: no cover return _broadcast_change(("delete", (k1, value))) _broadcast_change(("save", (k2, value))) # If we add a listener to a DirectoryBasedExampleDatabase whose database # directory doesn't yet exist, the watchdog observer will not fire any # events, even after the directory gets created. # # Ensure the directory exists before starting the observer. self.path.mkdir(exist_ok=True, parents=True) self._observer = Observer() self._observer.schedule( Handler(), # remove type: ignore when released # https://github.com/gorakhargosh/watchdog/pull/1096 self.path, # type: ignore recursive=True, event_filter=[FileCreatedEvent, FileDeletedEvent, FileMovedEvent], ) self._observer.start() def _stop_listening(self) -> None: assert self._observer is not None self._observer.stop() self._observer.join() self._observer = None class ReadOnlyDatabase(ExampleDatabase): """A wrapper to make the given database read-only. The implementation passes through ``fetch``, and turns ``save``, ``delete``, and ``move`` into silent no-ops. Note that this disables Hypothesis' automatic discarding of stale examples. It is designed to allow local machines to access a shared database (e.g. from CI servers), without propagating changes back from a local or in-development branch. """ def __init__(self, db: ExampleDatabase) -> None: super().__init__() assert isinstance(db, ExampleDatabase) self._wrapped = db def __repr__(self) -> str: return f"ReadOnlyDatabase({self._wrapped!r})" def __eq__(self, other: object) -> bool: return isinstance(other, ReadOnlyDatabase) and self._wrapped == other._wrapped def fetch(self, key: bytes) -> Iterable[bytes]: yield from self._wrapped.fetch(key) def save(self, key: bytes, value: bytes) -> None: pass def delete(self, key: bytes, value: bytes) -> None: pass def _start_listening(self) -> None: # we're read only, so there are no changes to broadcast. pass def _stop_listening(self) -> None: pass class MultiplexedDatabase(ExampleDatabase): """A wrapper around multiple databases. Each ``save``, ``fetch``, ``move``, or ``delete`` operation will be run against all of the wrapped databases. ``fetch`` does not yield duplicate values, even if the same value is present in two or more of the wrapped databases. This combines well with a :class:`ReadOnlyDatabase`, as follows: .. code-block:: python local = DirectoryBasedExampleDatabase("/tmp/hypothesis/examples/") shared = CustomNetworkDatabase() settings.register_profile("ci", database=shared) settings.register_profile( "dev", database=MultiplexedDatabase(local, ReadOnlyDatabase(shared)) ) settings.load_profile("ci" if os.environ.get("CI") else "dev") So your CI system or fuzzing runs can populate a central shared database; while local runs on development machines can reproduce any failures from CI but will only cache their own failures locally and cannot remove examples from the shared database. """ def __init__(self, *dbs: ExampleDatabase) -> None: super().__init__() assert all(isinstance(db, ExampleDatabase) for db in dbs) self._wrapped = dbs def __repr__(self) -> str: return "MultiplexedDatabase({})".format(", ".join(map(repr, self._wrapped))) def __eq__(self, other: object) -> bool: return ( isinstance(other, MultiplexedDatabase) and self._wrapped == other._wrapped ) def fetch(self, key: bytes) -> Iterable[bytes]: seen = set() for db in self._wrapped: for value in db.fetch(key): if value not in seen: yield value seen.add(value) def save(self, key: bytes, value: bytes) -> None: for db in self._wrapped: db.save(key, value) def delete(self, key: bytes, value: bytes) -> None: for db in self._wrapped: db.delete(key, value) def move(self, src: bytes, dest: bytes, value: bytes) -> None: for db in self._wrapped: db.move(src, dest, value) def _start_listening(self) -> None: for db in self._wrapped: db.add_listener(self._broadcast_change) def _stop_listening(self) -> None: for db in self._wrapped: db.remove_listener(self._broadcast_change) class GitHubArtifactDatabase(ExampleDatabase): """ A file-based database loaded from a `GitHub Actions `_ artifact. You can use this for sharing example databases between CI runs and developers, allowing the latter to get read-only access to the former. This is particularly useful for continuous fuzzing (i.e. with `HypoFuzz `_), where the CI system can help find new failing examples through fuzzing, and developers can reproduce them locally without any manual effort. .. note:: You must provide ``GITHUB_TOKEN`` as an environment variable. In CI, Github Actions provides this automatically, but it needs to be set manually for local usage. In a developer machine, this would usually be a `Personal Access Token `_. If the repository is private, it's necessary for the token to have ``repo`` scope in the case of a classic token, or ``actions:read`` in the case of a fine-grained token. In most cases, this will be used through the :class:`~hypothesis.database.MultiplexedDatabase`, by combining a local directory-based database with this one. For example: .. code-block:: python local = DirectoryBasedExampleDatabase(".hypothesis/examples") shared = ReadOnlyDatabase(GitHubArtifactDatabase("user", "repo")) settings.register_profile("ci", database=local) settings.register_profile("dev", database=MultiplexedDatabase(local, shared)) # We don't want to use the shared database in CI, only to populate its local one. # which the workflow should then upload as an artifact. settings.load_profile("ci" if os.environ.get("CI") else "dev") .. note:: Because this database is read-only, you always need to wrap it with the :class:`ReadOnlyDatabase`. A setup like this can be paired with a GitHub Actions workflow including something like the following: .. code-block:: yaml - name: Download example database uses: dawidd6/action-download-artifact@v9 with: name: hypothesis-example-db path: .hypothesis/examples if_no_artifact_found: warn workflow_conclusion: completed - name: Run tests run: pytest - name: Upload example database uses: actions/upload-artifact@v3 if: always() with: name: hypothesis-example-db path: .hypothesis/examples In this workflow, we use `dawidd6/action-download-artifact `_ to download the latest artifact given that the official `actions/download-artifact `_ does not support downloading artifacts from previous workflow runs. The database automatically implements a simple file-based cache with a default expiration period of 1 day. You can adjust this through the ``cache_timeout`` property. For mono-repo support, you can provide a unique ``artifact_name`` (e.g. ``hypofuzz-example-db-frontend``). """ def __init__( self, owner: str, repo: str, artifact_name: str = "hypothesis-example-db", cache_timeout: timedelta = timedelta(days=1), path: StrPathT | None = None, ): super().__init__() self.owner = owner self.repo = repo self.artifact_name = artifact_name self.cache_timeout = cache_timeout # Get the GitHub token from the environment # It's unnecessary to use a token if the repo is public self.token: str | None = getenv("GITHUB_TOKEN") if path is None: self.path: Path = Path( storage_directory(f"github-artifacts/{self.artifact_name}/") ) else: self.path = Path(path) # We don't want to initialize the cache until we need to self._initialized: bool = False self._disabled: bool = False # This is the path to the artifact in usage # .hypothesis/github-artifacts//.zip self._artifact: Path | None = None # This caches the artifact structure self._access_cache: dict[PurePath, set[PurePath]] | None = None # Message to display if user doesn't wrap around ReadOnlyDatabase self._read_only_message = ( "This database is read-only. " "Please wrap this class with ReadOnlyDatabase" "i.e. ReadOnlyDatabase(GitHubArtifactDatabase(...))." ) def __repr__(self) -> str: return ( f"GitHubArtifactDatabase(owner={self.owner!r}, " f"repo={self.repo!r}, artifact_name={self.artifact_name!r})" ) def __eq__(self, other: object) -> bool: return ( isinstance(other, GitHubArtifactDatabase) and self.owner == other.owner and self.repo == other.repo and self.artifact_name == other.artifact_name and self.path == other.path ) def _prepare_for_io(self) -> None: assert self._artifact is not None, "Artifact not loaded." if self._initialized: # pragma: no cover return # Test that the artifact is valid try: with ZipFile(self._artifact) as f: if f.testzip(): # pragma: no cover raise BadZipFile # Turns out that testzip() doesn't work quite well # doing the cache initialization here instead # will give us more coverage of the artifact. # Cache the files inside each keypath self._access_cache = {} with ZipFile(self._artifact) as zf: namelist = zf.namelist() # Iterate over files in the artifact for filename in namelist: fileinfo = zf.getinfo(filename) if fileinfo.is_dir(): self._access_cache[PurePath(filename)] = set() else: # Get the keypath from the filename keypath = PurePath(filename).parent # Add the file to the keypath self._access_cache[keypath].add(PurePath(filename)) except BadZipFile: warnings.warn( "The downloaded artifact from GitHub is invalid. " "This could be because the artifact was corrupted, " "or because the artifact was not created by Hypothesis. ", HypothesisWarning, stacklevel=3, ) self._disabled = True self._initialized = True def _initialize_db(self) -> None: # Trigger warning that we suppressed earlier by intent_to_write=False storage_directory(self.path.name) # Create the cache directory if it doesn't exist self.path.mkdir(exist_ok=True, parents=True) # Get all artifacts cached_artifacts = sorted( self.path.glob("*.zip"), key=lambda a: datetime.fromisoformat(a.stem.replace("_", ":")), ) # Remove all but the latest artifact for artifact in cached_artifacts[:-1]: artifact.unlink() try: found_artifact = cached_artifacts[-1] except IndexError: found_artifact = None # Check if the latest artifact is a cache hit if found_artifact is not None and ( datetime.now(timezone.utc) - datetime.fromisoformat(found_artifact.stem.replace("_", ":")) < self.cache_timeout ): self._artifact = found_artifact else: # Download the latest artifact from GitHub new_artifact = self._fetch_artifact() if new_artifact: if found_artifact is not None: found_artifact.unlink() self._artifact = new_artifact elif found_artifact is not None: warnings.warn( "Using an expired artifact as a fallback for the database: " f"{found_artifact}", HypothesisWarning, stacklevel=2, ) self._artifact = found_artifact else: warnings.warn( "Couldn't acquire a new or existing artifact. Disabling database.", HypothesisWarning, stacklevel=2, ) self._disabled = True return self._prepare_for_io() def _get_bytes(self, url: str) -> bytes | None: # pragma: no cover request = Request( url, headers={ "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28 ", "Authorization": f"Bearer {self.token}", }, ) warning_message = None response_bytes: bytes | None = None try: with urlopen(request) as response: response_bytes = response.read() except HTTPError as e: if e.code == 401: warning_message = ( "Authorization failed when trying to download artifact from GitHub. " "Check that you have a valid GITHUB_TOKEN set in your environment." ) else: warning_message = ( "Could not get the latest artifact from GitHub. " "This could be because the repository " "or artifact does not exist. " ) # see https://github.com/python/cpython/issues/128734 e.close() except URLError: warning_message = "Could not connect to GitHub to get the latest artifact. " except TimeoutError: warning_message = ( "Could not connect to GitHub to get the latest artifact " "(connection timed out)." ) if warning_message is not None: warnings.warn(warning_message, HypothesisWarning, stacklevel=4) return None return response_bytes def _fetch_artifact(self) -> Path | None: # pragma: no cover # Get the list of artifacts from GitHub url = f"https://api.github.com/repos/{self.owner}/{self.repo}/actions/artifacts" response_bytes = self._get_bytes(url) if response_bytes is None: return None artifacts = json.loads(response_bytes)["artifacts"] artifacts = [a for a in artifacts if a["name"] == self.artifact_name] if not artifacts: return None # Get the latest artifact from the list artifact = max(artifacts, key=lambda a: a["created_at"]) url = artifact["archive_download_url"] # Download the artifact artifact_bytes = self._get_bytes(url) if artifact_bytes is None: return None # Save the artifact to the cache # We replace ":" with "_" to ensure the filenames are compatible # with Windows filesystems timestamp = datetime.now(timezone.utc).isoformat().replace(":", "_") artifact_path = self.path / f"{timestamp}.zip" try: artifact_path.write_bytes(artifact_bytes) except OSError: warnings.warn( "Could not save the latest artifact from GitHub. ", HypothesisWarning, stacklevel=3, ) return None return artifact_path @staticmethod @lru_cache def _key_path(key: bytes) -> PurePath: return PurePath(_hash(key) + "/") def fetch(self, key: bytes) -> Iterable[bytes]: if self._disabled: return if not self._initialized: self._initialize_db() if self._disabled: return assert self._artifact is not None assert self._access_cache is not None kp = self._key_path(key) with ZipFile(self._artifact) as zf: # Get all the files in the kp from the cache filenames = self._access_cache.get(kp, ()) for filename in filenames: with zf.open(filename.as_posix()) as f: yield f.read() # Read-only interface def save(self, key: bytes, value: bytes) -> None: raise RuntimeError(self._read_only_message) def move(self, src: bytes, dest: bytes, value: bytes) -> None: raise RuntimeError(self._read_only_message) def delete(self, key: bytes, value: bytes) -> None: raise RuntimeError(self._read_only_message) class BackgroundWriteDatabase(ExampleDatabase): """A wrapper which defers writes on the given database to a background thread. Calls to :meth:`~hypothesis.database.ExampleDatabase.fetch` wait for any enqueued writes to finish before fetching from the database. """ def __init__(self, db: ExampleDatabase) -> None: super().__init__() self._db = db self._queue: Queue[tuple[str, tuple[bytes, ...]]] = Queue() self._thread: Thread | None = None def _ensure_thread(self): if self._thread is None: self._thread = Thread(target=self._worker, daemon=True) self._thread.start() # avoid an unbounded timeout during gc. 0.1 should be plenty for most # use cases. weakref.finalize(self, self._join, 0.1) def __repr__(self) -> str: return f"BackgroundWriteDatabase({self._db!r})" def __eq__(self, other: object) -> bool: return isinstance(other, BackgroundWriteDatabase) and self._db == other._db def _worker(self) -> None: while True: method, args = self._queue.get() getattr(self._db, method)(*args) self._queue.task_done() def _join(self, timeout: float | None = None) -> None: # copy of Queue.join with a timeout. https://bugs.python.org/issue9634 with self._queue.all_tasks_done: while self._queue.unfinished_tasks: self._queue.all_tasks_done.wait(timeout) def fetch(self, key: bytes) -> Iterable[bytes]: self._join() return self._db.fetch(key) def save(self, key: bytes, value: bytes) -> None: self._ensure_thread() self._queue.put(("save", (key, value))) def delete(self, key: bytes, value: bytes) -> None: self._ensure_thread() self._queue.put(("delete", (key, value))) def move(self, src: bytes, dest: bytes, value: bytes) -> None: self._ensure_thread() self._queue.put(("move", (src, dest, value))) def _start_listening(self) -> None: self._db.add_listener(self._broadcast_change) def _stop_listening(self) -> None: self._db.remove_listener(self._broadcast_change) def _pack_uleb128(value: int) -> bytes: """ Serialize an integer into variable-length bytes. For each byte, the first 7 bits represent (part of) the integer, while the last bit indicates whether the integer continues into the next byte. https://en.wikipedia.org/wiki/LEB128 """ parts = bytearray() assert value >= 0 while True: # chop off 7 bits byte = value & ((1 << 7) - 1) value >>= 7 # set the continuation bit if we have more left if value: byte |= 1 << 7 parts.append(byte) if not value: break return bytes(parts) def _unpack_uleb128(buffer: bytes) -> tuple[int, int]: """ Inverts _pack_uleb128, and also returns the index at which at which we stopped reading. """ value = 0 for i, byte in enumerate(buffer): n = byte & ((1 << 7) - 1) value |= n << (i * 7) if not byte >> 7: break return (i + 1, value) def choices_to_bytes(choices: Iterable[ChoiceT], /) -> bytes: """Serialize a list of choices to a bytestring. Inverts choices_from_bytes.""" # We use a custom serialization format for this, which might seem crazy - but our # data is a flat sequence of elements, and standard tools like protobuf or msgpack # don't deal well with e.g. nonstandard bit-pattern-NaNs, or invalid-utf8 unicode. # # We simply encode each element with a metadata byte, if needed a uint16 size, and # then the payload bytes. For booleans, the payload is inlined into the metadata. parts = [] for choice in choices: if isinstance(choice, bool): # `000_0000v` - tag zero, low bit payload. parts.append(b"\1" if choice else b"\0") continue # `tag_ssss [uint16 size?] [payload]` if isinstance(choice, float): tag = 1 << 5 choice = struct.pack("!d", choice) elif isinstance(choice, int): tag = 2 << 5 choice = choice.to_bytes(1 + choice.bit_length() // 8, "big", signed=True) elif isinstance(choice, bytes): tag = 3 << 5 else: assert isinstance(choice, str) tag = 4 << 5 choice = choice.encode(errors="surrogatepass") size = len(choice) if size < 0b11111: parts.append((tag | size).to_bytes(1, "big")) else: parts.append((tag | 0b11111).to_bytes(1, "big")) parts.append(_pack_uleb128(size)) parts.append(choice) return b"".join(parts) def _choices_from_bytes(buffer: bytes, /) -> tuple[ChoiceT, ...]: # See above for an explanation of the format. parts: list[ChoiceT] = [] idx = 0 while idx < len(buffer): tag = buffer[idx] >> 5 size = buffer[idx] & 0b11111 idx += 1 if tag == 0: parts.append(bool(size)) continue if size == 0b11111: offset, size = _unpack_uleb128(buffer[idx:]) idx += offset chunk = buffer[idx : idx + size] idx += size if tag == 1: assert size == 8, "expected float64" parts.extend(struct.unpack("!d", chunk)) elif tag == 2: parts.append(int.from_bytes(chunk, "big", signed=True)) elif tag == 3: parts.append(chunk) else: assert tag == 4 parts.append(chunk.decode(errors="surrogatepass")) return tuple(parts) def choices_from_bytes(buffer: bytes, /) -> tuple[ChoiceT, ...] | None: """ Deserialize a bytestring to a tuple of choices. Inverts choices_to_bytes. Returns None if the given bytestring is not a valid serialization of choice sequences. """ try: return _choices_from_bytes(buffer) except Exception: # deserialization error, eg because our format changed or someone put junk # data in the db. return None ================================================ FILE: hypothesis-python/src/hypothesis/entry_points.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Run all functions registered for the "hypothesis" entry point. This can be used with `st.register_type_strategy` to register strategies for your custom types, running the relevant code when *hypothesis* is imported instead of your package. """ import importlib.metadata import os def run() -> None: if os.environ.get("HYPOTHESIS_NO_PLUGINS"): return for entry in importlib.metadata.entry_points( group="hypothesis" ): # pragma: no cover hook = entry.load() if callable(hook): hook() ================================================ FILE: hypothesis-python/src/hypothesis/errors.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from datetime import timedelta from typing import Any, Literal from hypothesis.internal.compat import ExceptionGroup class HypothesisException(Exception): """Generic parent class for exceptions thrown by Hypothesis.""" class _Trimmable(HypothesisException): """Hypothesis can trim these tracebacks even if they're raised internally.""" class UnsatisfiedAssumption(HypothesisException): """An internal error raised by assume. If you're seeing this error something has gone wrong. """ def __init__(self, reason: str | None = None) -> None: self.reason = reason class NoSuchExample(HypothesisException): """The condition we have been asked to satisfy appears to be always false. This does not guarantee that no example exists, only that we were unable to find one. """ def __init__(self, condition_string: str, extra: str = "") -> None: super().__init__(f"No examples found of condition {condition_string}{extra}") class Unsatisfiable(_Trimmable): """We ran out of time or examples before we could find enough examples which satisfy the assumptions of this hypothesis. This could be because the function is too slow. If so, try upping the timeout. It could also be because the function is using assume in a way that is too hard to satisfy. If so, try writing a custom strategy or using a better starting point (e.g if you are requiring a list has unique values you could instead filter out all duplicate values from the list) """ class ChoiceTooLarge(HypothesisException): """An internal error raised by choice_from_index.""" class Flaky(_Trimmable): """ Base class for indeterministic failures. Usually one of the more specific subclasses (|FlakyFailure| or |FlakyStrategyDefinition|) is raised. .. seealso:: See also the :doc:`flaky failures tutorial
`. """ class FlakyReplay(Flaky): """Internal error raised by the conjecture engine if flaky failures are detected during replay. Carries information allowing the runner to reconstruct the flakiness as a FlakyFailure exception group for final presentation. """ def __init__(self, reason, interesting_origins=None): super().__init__(reason) self.reason = reason self._interesting_origins = interesting_origins class FlakyStrategyDefinition(Flaky): """ This function appears to cause inconsistent data generation. Common causes for this problem are: 1. The strategy depends on external state. e.g. it uses an external random number generator. Try to make a version that passes all the relevant state in from Hypothesis. .. seealso:: See also the :doc:`flaky failures tutorial `. """ class _WrappedBaseException(Exception): """Used internally for wrapping BaseExceptions as components of FlakyFailure.""" class FlakyFailure(ExceptionGroup, Flaky): """ This function appears to fail non-deterministically: We have seen it fail when passed this example at least once, but a subsequent invocation did not fail, or caused a distinct error. Common causes for this problem are: 1. The function depends on external state. e.g. it uses an external random number generator. Try to make a version that passes all the relevant state in from Hypothesis. 2. The function is suffering from too much recursion and its failure depends sensitively on where it's been called from. 3. The function is timing sensitive and can fail or pass depending on how long it takes. Try breaking it up into smaller functions which don't do that and testing those instead. .. seealso:: See also the :doc:`flaky failures tutorial `. """ def __new__(cls, msg, group): # The Exception mixin forces this an ExceptionGroup (only accepting # Exceptions, not BaseException). Usually BaseException is raised # directly and will hence not be part of a FlakyFailure, but I'm not # sure this assumption holds everywhere. So wrap any BaseExceptions. group = list(group) for i, exc in enumerate(group): if not isinstance(exc, Exception): err = _WrappedBaseException() err.__cause__ = err.__context__ = exc group[i] = err return ExceptionGroup.__new__(cls, msg, group) # defining `derive` is required for `split` to return an instance of FlakyFailure # instead of ExceptionGroup. See https://github.com/python/cpython/issues/119287 # and https://docs.python.org/3/library/exceptions.html#BaseExceptionGroup.derive def derive(self, excs): return type(self)(self.message, excs) class FlakyBackendFailure(FlakyFailure): """ A failure was reported by an |alternative backend|, but this failure did not reproduce when replayed under the Hypothesis backend. When an alternative backend reports a failure, Hypothesis first replays it under the standard Hypothesis backend to check for flakiness. If the failure does not reproduce, Hypothesis raises this exception. """ class InvalidArgument(_Trimmable, TypeError): """Used to indicate that the arguments to a Hypothesis function were in some manner incorrect.""" class ResolutionFailed(InvalidArgument): """Hypothesis had to resolve a type to a strategy, but this failed. Type inference is best-effort, so this only happens when an annotation exists but could not be resolved for a required argument to the target of ``builds()``, or where the user passed ``...``. """ class InvalidState(HypothesisException): """The system is not in a state where you were allowed to do that.""" class InvalidDefinition(_Trimmable, TypeError): """Used to indicate that a class definition was not well put together and has something wrong with it.""" class HypothesisWarning(HypothesisException, Warning): """A generic warning issued by Hypothesis.""" class FailedHealthCheck(_Trimmable): """Raised when a test fails a health check. See |HealthCheck|.""" class NonInteractiveExampleWarning(HypothesisWarning): """ Emitted when |.example| is used outside of interactive use. |.example| is intended for exploratory and interactive work, not to be run as part of a test suite. """ class HypothesisDeprecationWarning(HypothesisWarning, FutureWarning): """A deprecation warning issued by Hypothesis. Actually inherits from FutureWarning, because DeprecationWarning is hidden by the default warnings filter. You can configure the :mod:`python:warnings` module to handle these warnings differently to others, either turning them into errors or suppressing them entirely. Obviously we would prefer the former! """ class HypothesisSideeffectWarning(HypothesisWarning): """A warning issued by Hypothesis when it sees actions that are discouraged at import or initialization time because they are slow or have user-visible side effects. """ class Frozen(HypothesisException): """Raised when a mutation method has been called on a ConjectureData object after freeze() has been called.""" def __getattr__(name: str) -> Any: if name == "MultipleFailures": from hypothesis.internal.compat import BaseExceptionGroup from hypothesis.utils.deprecation import note_deprecation note_deprecation( "MultipleFailures is deprecated; use the builtin `BaseExceptionGroup` type " "instead, or `exceptiongroup.BaseExceptionGroup` before Python 3.11", since="2022-08-02", has_codemod=False, # This would be a great PR though! stacklevel=1, ) return BaseExceptionGroup raise AttributeError(f"Module 'hypothesis.errors' has no attribute {name}") class DeadlineExceeded(_Trimmable): """ Raised when an input takes too long to run, relative to the |settings.deadline| setting. """ def __init__(self, runtime: timedelta, deadline: timedelta) -> None: super().__init__( f"Test took {runtime.total_seconds() * 1000:.2f}ms, which exceeds " f"the deadline of {deadline.total_seconds() * 1000:.2f}ms. If you " "expect test cases to take this long, you can use @settings(deadline=...) " "to either set a higher deadline, or to disable it with deadline=None." ) self.runtime = runtime self.deadline = deadline def __reduce__( self, ) -> tuple[type["DeadlineExceeded"], tuple[timedelta, timedelta]]: return (type(self), (self.runtime, self.deadline)) class StopTest(BaseException): """Raised when a test should stop running and return control to the Hypothesis engine, which should then continue normally. """ def __init__(self, testcounter: int) -> None: super().__init__(repr(testcounter)) self.testcounter = testcounter class DidNotReproduce(HypothesisException): pass class Found(HypothesisException): """Signal that the example matches condition. Internal use only.""" class RewindRecursive(Exception): """Signal that the type inference should be rewound due to recursive types. Internal use only.""" def __init__(self, target: object) -> None: self.target = target class SmallSearchSpaceWarning(HypothesisWarning): """Indicates that an inferred strategy does not span the search space in a meaningful way, for example by only creating default instances.""" CannotProceedScopeT = Literal["verified", "exhausted", "discard_test_case", "other"] _valid_cannot_proceed_scopes = CannotProceedScopeT.__args__ # type: ignore class BackendCannotProceed(HypothesisException): """ Raised by alternative backends when a |PrimitiveProvider| cannot proceed. This is expected to occur inside one of the ``.draw_*()`` methods, or for symbolic execution perhaps in |PrimitiveProvider.realize|. The optional ``scope`` argument can enable smarter integration: verified: Do not request further test cases from this backend. We *may* generate more test cases with other backends; if one fails then Hypothesis will report unsound verification in the backend too. exhausted: Do not request further test cases from this backend; finish testing with test cases generated with the default backend. Common if e.g. native code blocks symbolic reasoning very early. discard_test_case: This particular test case could not be converted to concrete values; skip any further processing and continue with another test case from this backend. """ def __init__(self, scope: CannotProceedScopeT = "other", /) -> None: if scope not in _valid_cannot_proceed_scopes: raise InvalidArgument( f"Got scope={scope}, but expected one of " f"{_valid_cannot_proceed_scopes!r}" ) self.scope = scope ================================================ FILE: hypothesis-python/src/hypothesis/extra/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/src/hypothesis/extra/_array_helpers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re from types import EllipsisType from typing import NamedTuple from hypothesis import assume, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture.utils import _calc_p_continue from hypothesis.internal.coverage import check_function from hypothesis.internal.validation import check_type, check_valid_interval from hypothesis.strategies._internal.utils import defines_strategy from hypothesis.utils.conventions import UniqueIdentifier, not_set __all__ = [ "NDIM_MAX", "_BIE", "BasicIndex", "BasicIndexStrategy", "BroadcastableShapes", "MutuallyBroadcastableShapesStrategy", "Shape", "_BIENoEllipsis", "_BIENoEllipsisNoNewaxis", "_BIENoNewaxis", "array_shapes", "broadcastable_shapes", "check_argument", "check_valid_dims", "mutually_broadcastable_shapes", "order_check", "valid_tuple_axes", ] Shape = tuple[int, ...] # Type aliases for basic array index elements. Variants exist to accurately # type the return value of basic_indices() based on allow_ellipsis/allow_newaxis. _BIE = int | slice | None | EllipsisType _BIENoEllipsis = int | slice | None _BIENoNewaxis = int | slice | EllipsisType _BIENoEllipsisNoNewaxis = int | slice BasicIndex = _BIE | tuple[_BIE, ...] class BroadcastableShapes(NamedTuple): input_shapes: tuple[Shape, ...] result_shape: Shape @check_function def check_argument(condition, fail_message, *f_args, **f_kwargs): if not condition: raise InvalidArgument(fail_message.format(*f_args, **f_kwargs)) @check_function def order_check(name, floor, min_, max_): if floor > min_: raise InvalidArgument(f"min_{name} must be at least {floor} but was {min_}") if min_ > max_: raise InvalidArgument(f"min_{name}={min_} is larger than max_{name}={max_}") # 32 is a dimension limit specific to NumPy, and does not necessarily apply to # other array/tensor libraries. Historically these strategies were built for the # NumPy extra, so it's nice to keep these limits, and it's seemingly unlikely # someone would want to generate >32 dim arrays anyway. # See https://github.com/HypothesisWorks/hypothesis/pull/3067. NDIM_MAX = 32 @check_function def check_valid_dims(dims, name): if dims > NDIM_MAX: raise InvalidArgument( f"{name}={dims}, but Hypothesis does not support arrays with " f"more than {NDIM_MAX} dimensions" ) @defines_strategy() def array_shapes( *, min_dims: int = 1, max_dims: int | None = None, min_side: int = 1, max_side: int | None = None, ) -> st.SearchStrategy[Shape]: """Return a strategy for array shapes (tuples of int >= 1). * ``min_dims`` is the smallest length that the generated shape can possess. * ``max_dims`` is the largest length that the generated shape can possess, defaulting to ``min_dims + 2``. * ``min_side`` is the smallest size that a dimension can possess. * ``max_side`` is the largest size that a dimension can possess, defaulting to ``min_side + 5``. """ check_type(int, min_dims, "min_dims") check_type(int, min_side, "min_side") check_valid_dims(min_dims, "min_dims") if max_dims is None: max_dims = min(min_dims + 2, NDIM_MAX) check_type(int, max_dims, "max_dims") check_valid_dims(max_dims, "max_dims") if max_side is None: max_side = min_side + 5 check_type(int, max_side, "max_side") order_check("dims", 0, min_dims, max_dims) order_check("side", 0, min_side, max_side) return st.lists( st.integers(min_side, max_side), min_size=min_dims, max_size=max_dims ).map(tuple) @defines_strategy() def valid_tuple_axes( ndim: int, *, min_size: int = 0, max_size: int | None = None, ) -> st.SearchStrategy[tuple[int, ...]]: """ All tuples will have a length >= ``min_size`` and <= ``max_size``. The default value for ``max_size`` is ``ndim``. Examples from this strategy shrink towards an empty tuple, which render most sequential functions as no-ops. The following are some examples drawn from this strategy. .. code-block:: pycon >>> [valid_tuple_axes(3).example() for i in range(4)] [(-3, 1), (0, 1, -1), (0, 2), (0, -2, 2)] ``valid_tuple_axes`` can be joined with other strategies to generate any type of valid axis object, i.e. integers, tuples, and ``None``: .. code-block:: python any_axis_strategy = none() | integers(-ndim, ndim - 1) | valid_tuple_axes(ndim) """ check_type(int, ndim, "ndim") check_type(int, min_size, "min_size") if max_size is None: max_size = ndim check_type(int, max_size, "max_size") order_check("size", 0, min_size, max_size) check_valid_interval(max_size, ndim, "max_size", "ndim") axes = st.integers(0, max(0, 2 * ndim - 1)).map( lambda x: x if x < ndim else x - 2 * ndim ) return st.lists( axes, min_size=min_size, max_size=max_size, unique_by=lambda x: x % ndim ).map(tuple) @defines_strategy() def broadcastable_shapes( shape: Shape, *, min_dims: int = 0, max_dims: int | None = None, min_side: int = 1, max_side: int | None = None, ) -> st.SearchStrategy[Shape]: """Return a strategy for shapes that are broadcast-compatible with the provided shape. Examples from this strategy shrink towards a shape with length ``min_dims``. The size of an aligned dimension shrinks towards size ``1``. The size of an unaligned dimension shrink towards ``min_side``. * ``shape`` is a tuple of integers. * ``min_dims`` is the smallest length that the generated shape can possess. * ``max_dims`` is the largest length that the generated shape can possess, defaulting to ``max(len(shape), min_dims) + 2``. * ``min_side`` is the smallest size that an unaligned dimension can possess. * ``max_side`` is the largest size that an unaligned dimension can possess, defaulting to 2 plus the size of the largest aligned dimension. The following are some examples drawn from this strategy. .. code-block:: pycon >>> [broadcastable_shapes(shape=(2, 3)).example() for i in range(5)] [(1, 3), (), (2, 3), (2, 1), (4, 1, 3), (3, )] """ check_type(tuple, shape, "shape") check_type(int, min_side, "min_side") check_type(int, min_dims, "min_dims") check_valid_dims(min_dims, "min_dims") strict_check = max_side is None or max_dims is None if max_dims is None: max_dims = min(max(len(shape), min_dims) + 2, NDIM_MAX) check_type(int, max_dims, "max_dims") check_valid_dims(max_dims, "max_dims") if max_side is None: max_side = max(shape[-max_dims:] + (min_side,)) + 2 check_type(int, max_side, "max_side") order_check("dims", 0, min_dims, max_dims) order_check("side", 0, min_side, max_side) if strict_check: dims = max_dims bound_name = "max_dims" else: dims = min_dims bound_name = "min_dims" # check for unsatisfiable min_side if not all(min_side <= s for s in shape[::-1][:dims] if s != 1): raise InvalidArgument( f"Given shape={shape}, there are no broadcast-compatible " f"shapes that satisfy: {bound_name}={dims} and min_side={min_side}" ) # check for unsatisfiable [min_side, max_side] if not ( min_side <= 1 <= max_side or all(s <= max_side for s in shape[::-1][:dims]) ): raise InvalidArgument( f"Given base_shape={shape}, there are no broadcast-compatible " f"shapes that satisfy all of {bound_name}={dims}, " f"min_side={min_side}, and max_side={max_side}" ) if not strict_check: # reduce max_dims to exclude unsatisfiable dimensions for n, s in zip(range(max_dims), shape[::-1], strict=False): if s < min_side and s != 1: max_dims = n break if not (min_side <= 1 <= max_side or s <= max_side): max_dims = n break return MutuallyBroadcastableShapesStrategy( num_shapes=1, base_shape=shape, min_dims=min_dims, max_dims=max_dims, min_side=min_side, max_side=max_side, ).map(lambda x: x.input_shapes[0]) # See https://numpy.org/doc/stable/reference/c-api/generalized-ufuncs.html # Implementation based on numpy.lib.function_base._parse_gufunc_signature # with minor upgrades to handle numeric and optional dimensions. Examples: # # add (),()->() binary ufunc # sum1d (i)->() reduction # inner1d (i),(i)->() vector-vector multiplication # matmat (m,n),(n,p)->(m,p) matrix multiplication # vecmat (n),(n,p)->(p) vector-matrix multiplication # matvec (m,n),(n)->(m) matrix-vector multiplication # matmul (m?,n),(n,p?)->(m?,p?) combination of the four above # cross1d (3),(3)->(3) cross product with frozen dimensions # # Note that while no examples of such usage are given, Numpy does allow # generalised ufuncs that have *multiple output arrays*. This is not # currently supported by Hypothesis - please contact us if you would use it! # # We are unsure if gufuncs allow frozen dimensions to be optional, but it's # easy enough to support here - and so we will unless we learn otherwise. _DIMENSION = r"\w+\??" # Note that \w permits digits too! _SHAPE = rf"\((?:{_DIMENSION}(?:,{_DIMENSION}){{0,31}})?\)" _ARGUMENT_LIST = f"{_SHAPE}(?:,{_SHAPE})*" _SIGNATURE = rf"^{_ARGUMENT_LIST}->{_SHAPE}$" _SIGNATURE_MULTIPLE_OUTPUT = rf"^{_ARGUMENT_LIST}->{_ARGUMENT_LIST}$" class _GUfuncSig(NamedTuple): input_shapes: tuple[Shape, ...] result_shape: Shape def _hypothesis_parse_gufunc_signature(signature): # Disable all_checks to better match the Numpy version, for testing if not re.match(_SIGNATURE, signature): if re.match(_SIGNATURE_MULTIPLE_OUTPUT, signature): raise InvalidArgument( "Hypothesis does not yet support generalised ufunc signatures " "with multiple output arrays - mostly because we don't know of " "anyone who uses them! Please get in touch with us to fix that." f"\n ({signature=})" ) if re.match( ( # Taken from np.lib.function_base._SIGNATURE r"^\((?:\w+(?:,\w+)*)?\)(?:,\((?:\w+(?:,\w+)*)?\))*->" r"\((?:\w+(?:,\w+)*)?\)(?:,\((?:\w+(?:,\w+)*)?\))*$" ), signature, ): raise InvalidArgument( f"{signature=} matches Numpy's regex for gufunc signatures, " f"but contains shapes with more than {NDIM_MAX} dimensions and is thus invalid." ) raise InvalidArgument(f"{signature!r} is not a valid gufunc signature") input_shapes, output_shapes = ( tuple(tuple(re.findall(_DIMENSION, a)) for a in re.findall(_SHAPE, arg_list)) for arg_list in signature.split("->") ) assert len(output_shapes) == 1 result_shape = output_shapes[0] # Check that there are no names in output shape that do not appear in inputs. # (kept out of parser function for easier generation of test values) # We also disallow frozen optional dimensions - this is ambiguous as there is # no way to share an un-named dimension between shapes. Maybe just padding? # Anyway, we disallow it pending clarification from upstream. for shape in (*input_shapes, result_shape): for name in shape: try: int(name.strip("?")) if "?" in name: raise InvalidArgument( f"Got dimension {name!r}, but handling of frozen optional dimensions " "is ambiguous. If you known how this should work, please " f"contact us to get this fixed and documented ({signature=})." ) except ValueError: names_in = {n.strip("?") for shp in input_shapes for n in shp} names_out = {n.strip("?") for n in result_shape} if name.strip("?") in (names_out - names_in): raise InvalidArgument( f"The {name!r} dimension only appears in the output shape, and is " f"not frozen, so the size is not determined ({signature=})." ) from None return _GUfuncSig(input_shapes=input_shapes, result_shape=result_shape) @defines_strategy() def mutually_broadcastable_shapes( *, num_shapes: UniqueIdentifier | int = not_set, signature: UniqueIdentifier | str = not_set, base_shape: Shape = (), min_dims: int = 0, max_dims: int | None = None, min_side: int = 1, max_side: int | None = None, ) -> st.SearchStrategy[BroadcastableShapes]: """ Return a strategy for a specified number of shapes N that are mutually-broadcastable with one another and with the provided base shape. * ``num_shapes`` is the number of mutually broadcast-compatible shapes to generate. * ``base_shape`` is the shape against which all generated shapes can broadcast. The default shape is empty, which corresponds to a scalar and thus does not constrain broadcasting at all. * ``min_dims`` is the smallest length that the generated shape can possess. * ``max_dims`` is the largest length that the generated shape can possess, defaulting to ``max(len(shape), min_dims) + 2``. * ``min_side`` is the smallest size that an unaligned dimension can possess. * ``max_side`` is the largest size that an unaligned dimension can possess, defaulting to 2 plus the size of the largest aligned dimension. The strategy will generate a :obj:`python:typing.NamedTuple` containing: * ``input_shapes`` as a tuple of the N generated shapes. * ``result_shape`` as the resulting shape produced by broadcasting the N shapes with the base shape. The following are some examples drawn from this strategy. .. code-block:: pycon >>> # Draw three shapes where each shape is broadcast-compatible with (2, 3) ... strat = mutually_broadcastable_shapes(num_shapes=3, base_shape=(2, 3)) >>> for _ in range(5): ... print(strat.example()) BroadcastableShapes(input_shapes=((4, 1, 3), (4, 2, 3), ()), result_shape=(4, 2, 3)) BroadcastableShapes(input_shapes=((3,), (1, 3), (2, 3)), result_shape=(2, 3)) BroadcastableShapes(input_shapes=((), (), ()), result_shape=()) BroadcastableShapes(input_shapes=((3,), (), (3,)), result_shape=(3,)) BroadcastableShapes(input_shapes=((1, 2, 3), (3,), ()), result_shape=(1, 2, 3)) """ arg_msg = "Pass either the `num_shapes` or the `signature` argument, but not both." if num_shapes is not not_set: check_argument(signature is not_set, arg_msg) check_type(int, num_shapes, "num_shapes") assert isinstance(num_shapes, int) # for mypy parsed_signature = None sig_dims = 0 else: check_argument(signature is not not_set, arg_msg) if signature is None: raise InvalidArgument( "Expected a string, but got invalid signature=None. " "(maybe .signature attribute of an element-wise ufunc?)" ) check_type(str, signature, "signature") parsed_signature = _hypothesis_parse_gufunc_signature(signature) all_shapes = (*parsed_signature.input_shapes, parsed_signature.result_shape) sig_dims = min(len(s) for s in all_shapes) num_shapes = len(parsed_signature.input_shapes) if num_shapes < 1: raise InvalidArgument(f"num_shapes={num_shapes} must be at least 1") check_type(tuple, base_shape, "base_shape") check_type(int, min_side, "min_side") check_type(int, min_dims, "min_dims") check_valid_dims(min_dims, "min_dims") strict_check = max_dims is not None if max_dims is None: max_dims = min(max(len(base_shape), min_dims) + 2, NDIM_MAX - sig_dims) check_type(int, max_dims, "max_dims") check_valid_dims(max_dims, "max_dims") if max_side is None: max_side = max(base_shape[-max_dims:] + (min_side,)) + 2 check_type(int, max_side, "max_side") order_check("dims", 0, min_dims, max_dims) order_check("side", 0, min_side, max_side) if signature is not None and max_dims > NDIM_MAX - sig_dims: raise InvalidArgument( f"max_dims={signature!r} would exceed the {NDIM_MAX}-dimension" "limit Hypothesis imposes on array shapes, " f"given signature={parsed_signature!r}" ) if strict_check: dims = max_dims bound_name = "max_dims" else: dims = min_dims bound_name = "min_dims" # check for unsatisfiable min_side if not all(min_side <= s for s in base_shape[::-1][:dims] if s != 1): raise InvalidArgument( f"Given base_shape={base_shape}, there are no broadcast-compatible " f"shapes that satisfy: {bound_name}={dims} and min_side={min_side}" ) # check for unsatisfiable [min_side, max_side] if not ( min_side <= 1 <= max_side or all(s <= max_side for s in base_shape[::-1][:dims]) ): raise InvalidArgument( f"Given base_shape={base_shape}, there are no broadcast-compatible " f"shapes that satisfy all of {bound_name}={dims}, " f"min_side={min_side}, and max_side={max_side}" ) if not strict_check: # reduce max_dims to exclude unsatisfiable dimensions for n, s in zip(range(max_dims), base_shape[::-1], strict=False): if s < min_side and s != 1: max_dims = n break if not (min_side <= 1 <= max_side or s <= max_side): max_dims = n break return MutuallyBroadcastableShapesStrategy( num_shapes=num_shapes, signature=parsed_signature, base_shape=base_shape, min_dims=min_dims, max_dims=max_dims, min_side=min_side, max_side=max_side, ) class MutuallyBroadcastableShapesStrategy(st.SearchStrategy): def __init__( self, num_shapes, signature=None, base_shape=(), min_dims=0, max_dims=None, min_side=1, max_side=None, ): super().__init__() self.base_shape = base_shape self.side_strat = st.integers(min_side, max_side) self.num_shapes = num_shapes self.signature = signature self.min_dims = min_dims self.max_dims = max_dims self.min_side = min_side self.max_side = max_side self.size_one_allowed = self.min_side <= 1 <= self.max_side def do_draw(self, data): # We don't usually have a gufunc signature; do the common case first & fast. if self.signature is None: return self._draw_loop_dimensions(data) # When we *do*, draw the core dims, then draw loop dims, and finally combine. core_in, core_res = self._draw_core_dimensions(data) # If some core shape has omitted optional dimensions, it's an error to add # loop dimensions to it. We never omit core dims if min_dims >= 1. # This ensures that we respect Numpy's gufunc broadcasting semantics and user # constraints without needing to check whether the loop dims will be # interpreted as an invalid substitute for the omitted core dims. # We may implement this check later! use = [None not in shp for shp in core_in] loop_in, loop_res = self._draw_loop_dimensions(data, use=use) def add_shape(loop, core): return tuple(x for x in (loop + core)[-NDIM_MAX:] if x is not None) return BroadcastableShapes( input_shapes=tuple( add_shape(l_in, c) for l_in, c in zip(loop_in, core_in, strict=True) ), result_shape=add_shape(loop_res, core_res), ) def _draw_core_dimensions(self, data): # Draw gufunc core dimensions, with None standing for optional dimensions # that will not be present in the final shape. We track omitted dims so # that we can do an accurate per-shape length cap. dims = {} shapes = [] for shape in (*self.signature.input_shapes, self.signature.result_shape): shapes.append([]) for name in shape: if name.isdigit(): shapes[-1].append(int(name)) continue if name not in dims: dim = name.strip("?") dims[dim] = data.draw(self.side_strat) if self.min_dims == 0 and not data.draw_boolean(7 / 8): dims[dim + "?"] = None else: dims[dim + "?"] = dims[dim] shapes[-1].append(dims[name]) return tuple(tuple(s) for s in shapes[:-1]), tuple(shapes[-1]) def _draw_loop_dimensions(self, data, use=None): # All shapes are handled in column-major order; i.e. they are reversed base_shape = self.base_shape[::-1] result_shape = list(base_shape) shapes = [[] for _ in range(self.num_shapes)] if use is None: use = [True for _ in range(self.num_shapes)] else: assert len(use) == self.num_shapes assert all(isinstance(x, bool) for x in use) _gap = self.max_dims - self.min_dims p_keep_extending_shape = _calc_p_continue(desired_avg=_gap / 2, max_size=_gap) for dim_count in range(1, self.max_dims + 1): dim = dim_count - 1 # We begin by drawing a valid dimension-size for the given # dimension. This restricts the variability across the shapes # at this dimension such that they can only choose between # this size and a singleton dimension. if len(base_shape) < dim_count or base_shape[dim] == 1: # dim is unrestricted by the base-shape: shrink to min_side dim_side = data.draw(self.side_strat) elif base_shape[dim] <= self.max_side: # dim is aligned with non-singleton base-dim dim_side = base_shape[dim] else: # only a singleton is valid in alignment with the base-dim dim_side = 1 allowed_sides = sorted([1, dim_side]) # shrink to 0 when available for shape_id, shape in enumerate(shapes): # Populating this dimension-size for each shape, either # the drawn size is used or, if permitted, a singleton # dimension. if dim <= len(result_shape) and self.size_one_allowed: # aligned: shrink towards size 1 side = data.draw(st.sampled_from(allowed_sides)) else: side = dim_side # Use a trick where a biased coin is queried to see # if the given shape-tuple will continue to be grown. All # of the relevant draws will still be made for the given # shape-tuple even if it is no longer being added to. # This helps to ensure more stable shrinking behavior. if self.min_dims < dim_count: use[shape_id] &= data.draw_boolean(p_keep_extending_shape) if use[shape_id]: shape.append(side) if len(result_shape) < len(shape): result_shape.append(shape[-1]) elif shape[-1] != 1 and result_shape[dim] == 1: result_shape[dim] = shape[-1] if not any(use): break result_shape = result_shape[: max(map(len, [self.base_shape, *shapes]))] assert len(shapes) == self.num_shapes assert all(self.min_dims <= len(s) <= self.max_dims for s in shapes) assert all(self.min_side <= s <= self.max_side for side in shapes for s in side) return BroadcastableShapes( input_shapes=tuple(tuple(reversed(shape)) for shape in shapes), result_shape=tuple(reversed(result_shape)), ) class BasicIndexStrategy(st.SearchStrategy): def __init__( self, shape, min_dims, max_dims, allow_ellipsis, allow_newaxis, allow_fewer_indices_than_dims, ): super().__init__() self.shape = shape self.min_dims = min_dims self.max_dims = max_dims self.allow_ellipsis = allow_ellipsis self.allow_newaxis = allow_newaxis # allow_fewer_indices_than_dims=False will disable generating indices # that don't cover all axes, i.e. indices that will flat index arrays. # This is necessary for the Array API as such indices are not supported. self.allow_fewer_indices_than_dims = allow_fewer_indices_than_dims def do_draw(self, data): # General plan: determine the actual selection up front with a straightforward # approach that shrinks well, then complicate it by inserting other things. result = [] for dim_size in self.shape: if dim_size == 0: result.append(slice(None)) continue strategy = st.integers(-dim_size, dim_size - 1) | st.slices(dim_size) result.append(data.draw(strategy)) # Insert some number of new size-one dimensions if allowed result_dims = sum(isinstance(idx, slice) for idx in result) while ( self.allow_newaxis and result_dims < self.max_dims and (result_dims < self.min_dims or data.draw(st.booleans())) ): i = data.draw(st.integers(0, len(result))) result.insert(i, None) # Note that `np.newaxis is None` result_dims += 1 # Check that we'll have the right number of dimensions; reject if not. # It's easy to do this by construction if you don't care about shrinking, # which is really important for array shapes. So we filter instead. assume(self.min_dims <= result_dims <= self.max_dims) # This is a quick-and-dirty way to insert ..., xor shorten the indexer, # but it means we don't have to do any structural analysis. if self.allow_ellipsis and data.draw(st.booleans()): # Choose an index; then replace all adjacent whole-dimension slices. i = j = data.draw(st.integers(0, len(result))) while i > 0 and result[i - 1] == slice(None): i -= 1 while j < len(result) and result[j] == slice(None): j += 1 result[i:j] = [Ellipsis] elif self.allow_fewer_indices_than_dims: # pragma: no cover while result[-1:] == [slice(None, None)] and data.draw(st.integers(0, 7)): result.pop() if len(result) == 1 and data.draw(st.booleans()): # Sometimes generate bare element equivalent to a length-one tuple return result[0] return tuple(result) ================================================ FILE: hypothesis-python/src/hypothesis/extra/_patching.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ Write patches which add @example() decorators for discovered test cases. Requires `hypothesis[codemods,ghostwriter]` installed, i.e. black and libcst. This module is used by Hypothesis' builtin pytest plugin for failing examples discovered during testing, and by HypoFuzz for _covering_ examples discovered during fuzzing. """ import ast import difflib import hashlib import inspect import re import sys import types from ast import literal_eval from collections.abc import Sequence from contextlib import suppress from datetime import date, datetime, timedelta, timezone from pathlib import Path from typing import Any import libcst as cst from libcst import matchers as m from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand from libcst.metadata import ExpressionContext, ExpressionContextProvider from hypothesis.configuration import storage_directory from hypothesis.version import __version__ try: import black except ImportError: black = None # type: ignore HEADER = """\ From HEAD Mon Sep 17 00:00:00 2001 From: {author} Date: {when:%a, %d %b %Y %H:%M:%S} Subject: [PATCH] {msg} --- """ FAIL_MSG = "discovered failure" _space_only_re = re.compile("^ +$", re.MULTILINE) _leading_space_re = re.compile("(^[ ]*)(?:[^ \n])", re.MULTILINE) def dedent(text: str) -> tuple[str, str]: # Simplified textwrap.dedent, for valid Python source code only text = _space_only_re.sub("", text) prefix = min(_leading_space_re.findall(text), key=len) return re.sub(r"(?m)^" + prefix, "", text), prefix def indent(text: str, prefix: str) -> str: return "".join(prefix + line for line in text.splitlines(keepends=True)) class AddExamplesCodemod(VisitorBasedCodemodCommand): DESCRIPTION = "Add explicit examples to failing tests." def __init__( self, context: CodemodContext, fn_examples: dict[str, list[tuple[cst.Call, str]]], strip_via: tuple[str, ...] = (), decorator: str = "example", width: int = 88, ): """Add @example() decorator(s) for failing test(s). `code` is the source code of the module where the test functions are defined. `fn_examples` is a dict of function name to list-of-failing-examples. """ assert fn_examples, "This codemod does nothing without fn_examples." super().__init__(context) self.decorator_func = cst.parse_expression(decorator) self.line_length = width value_in_strip_via: Any = m.MatchIfTrue( lambda x: literal_eval(x.value) in strip_via ) self.strip_matching = m.Call( m.Attribute(m.Call(), m.Name("via")), [m.Arg(m.SimpleString() & value_in_strip_via)], ) # Codemod the failing examples to Call nodes usable as decorators self.fn_examples = { k: tuple( d for (node, via) in nodes if (d := self.__call_node_to_example_dec(node, via)) ) for k, nodes in fn_examples.items() } def __call_node_to_example_dec( self, node: cst.Call, via: str ) -> cst.Decorator | None: # If we have black installed, remove trailing comma, _unless_ there's a comment node = node.with_changes( func=self.decorator_func, args=( [ a.with_changes( comma=( a.comma if m.findall(a.comma, m.Comment()) else cst.MaybeSentinel.DEFAULT ) ) for a in node.args ] if black else node.args ), ) via: cst.BaseExpression = cst.Call( func=cst.Attribute(node, cst.Name("via")), args=[cst.Arg(cst.SimpleString(repr(via)))], ) if black: # pragma: no branch try: pretty = black.format_str( cst.Module([]).code_for_node(via), mode=black.Mode(line_length=self.line_length), ) except (ImportError, AttributeError): # pragma: no cover return None # See https://github.com/psf/black/pull/4224 via = cst.parse_expression(pretty.strip()) return cst.Decorator(via) def leave_FunctionDef( self, _original_node: cst.FunctionDef, updated_node: cst.FunctionDef ) -> cst.FunctionDef: return updated_node.with_changes( # TODO: improve logic for where in the list to insert this decorator decorators=tuple( d for d in updated_node.decorators # `findall()` to see through the identity function workaround on py38 if not m.findall(d, self.strip_matching) ) + self.fn_examples.get(updated_node.name.value, ()) ) def get_patch_for( func: Any, examples: Sequence[tuple[str, str]], *, strip_via: tuple[str, ...] = (), ) -> tuple[str, str, str] | None: # Skip this if we're unable to find the location of this function. try: module = sys.modules[func.__module__] file_path = Path(module.__file__) # type: ignore except Exception: return None fname = ( file_path.relative_to(Path.cwd()) if file_path.is_relative_to(Path.cwd()) else file_path ) patch = _get_patch_for( func, examples, strip_via=strip_via, namespace=module.__dict__ ) if patch is None: return None (before, after) = patch return (str(fname), before, after) # split out for easier testing of patches in hypofuzz, where the function to # apply the patch to may not be loaded in sys.modules. def _get_patch_for( func: Any, examples: Sequence[tuple[str, str]], *, strip_via: tuple[str, ...] = (), namespace: dict[str, Any], ) -> tuple[str, str] | None: try: before = inspect.getsource(func) except Exception: # pragma: no cover return None modules_in_test_scope = sorted( ((k, v) for (k, v) in namespace.items() if isinstance(v, types.ModuleType)), key=lambda kv: len(kv[1].__name__), ) # The printed examples might include object reprs which are invalid syntax, # so we parse here and skip over those. If _none_ are valid, there's no patch. call_nodes: list[tuple[cst.Call, str]] = [] # we want to preserve order, but remove duplicates. seen_examples = set() for ex, via in examples: if (ex, via) in seen_examples: continue seen_examples.add((ex, via)) with suppress(Exception): node: Any = cst.parse_module(ex) the_call = node.body[0].body[0].value assert isinstance(the_call, cst.Call), the_call # Check for st.data(), which doesn't support explicit examples data = m.Arg(m.Call(m.Name("data"), args=[m.Arg(m.Ellipsis())])) if m.matches(the_call, m.Call(args=[m.ZeroOrMore(), data, m.ZeroOrMore()])): return None # Many reprs use the unqualified name of the type, e.g. np.array() # -> array([...]), so here we find undefined names and look them up # on each module which was in the test's global scope. names = {} for anode in ast.walk(ast.parse(ex, "eval")): if ( isinstance(anode, ast.Name) and isinstance(anode.ctx, ast.Load) and anode.id not in names and anode.id not in namespace ): for k, v in modules_in_test_scope: if anode.id in v.__dict__: names[anode.id] = cst.parse_expression(f"{k}.{anode.id}") break # LibCST doesn't track Load()/Store() state of names by default, so we have # to do a bit of a dance here, *and* explicitly handle keyword arguments # which are treated as Load() context - but even if that's fixed later # we'll still want to support older versions. with suppress(Exception): wrapper = cst.metadata.MetadataWrapper(node) kwarg_names = { node.keyword # type: ignore for node in m.findall(wrapper, m.Arg(keyword=m.Name())) } node = m.replace( wrapper, m.Name(value=m.MatchIfTrue(names.__contains__)) & m.MatchMetadata(ExpressionContextProvider, ExpressionContext.LOAD) & m.MatchIfTrue(lambda n, k=kwarg_names: n not in k), # type: ignore replacement=lambda node, _, ns=names: ns[node.value], # type: ignore ) node = node.body[0].body[0].value assert isinstance(node, cst.Call), node call_nodes.append((node, via)) if not call_nodes: return None if ( namespace.get("hypothesis") is sys.modules["hypothesis"] and "given" not in namespace # more reliably present than `example` ): decorator_func = "hypothesis.example" else: decorator_func = "example" # Do the codemod and return a triple containing location and replacement info. dedented, prefix = dedent(before) try: node = cst.parse_module(dedented) except Exception: # pragma: no cover # inspect.getsource() sometimes returns a decorator alone, which is invalid return None after = AddExamplesCodemod( CodemodContext(), fn_examples={func.__name__: call_nodes}, strip_via=strip_via, decorator=decorator_func, width=88 - len(prefix), # to match Black's default formatting ).transform_module(node) return (before, indent(after.code, prefix=prefix)) def make_patch( triples: Sequence[tuple[str, str, str]], *, msg: str = "Hypothesis: add explicit examples", when: datetime | None = None, author: str = f"Hypothesis {__version__} ", ) -> str: """Create a patch for (fname, before, after) triples.""" assert triples, "attempted to create empty patch" when = when or datetime.now(tz=timezone.utc) by_fname: dict[Path, list[tuple[str, str]]] = {} for fname, before, after in triples: by_fname.setdefault(Path(fname), []).append((before, after)) diffs = [HEADER.format(msg=msg, when=when, author=author)] for fname, changes in sorted(by_fname.items()): source_before = source_after = fname.read_text(encoding="utf-8") for before, after in changes: source_after = source_after.replace(before.rstrip(), after.rstrip(), 1) ud = difflib.unified_diff( source_before.splitlines(keepends=True), source_after.splitlines(keepends=True), fromfile=f"./{fname}", # git strips the first part of the path by default tofile=f"./{fname}", ) diffs.append("".join(ud)) return "".join(diffs) def save_patch(patch: str, *, slug: str = "") -> Path: # pragma: no cover assert re.fullmatch(r"|[a-z]+-", slug), f"malformed {slug=}" now = date.today().isoformat() cleaned = re.sub(r"^Date: .+?$", "", patch, count=1, flags=re.MULTILINE) hash8 = hashlib.sha1(cleaned.encode()).hexdigest()[:8] fname = Path(storage_directory("patches", f"{now}--{slug}{hash8}.patch")) fname.parent.mkdir(parents=True, exist_ok=True) fname.write_text(patch, encoding="utf-8") return fname.relative_to(Path.cwd()) def gc_patches(slug: str = "") -> None: # pragma: no cover cutoff = date.today() - timedelta(days=7) for fname in Path(storage_directory("patches")).glob( f"????-??-??--{slug}????????.patch" ): if date.fromisoformat(fname.stem.split("--")[0]) < cutoff: fname.unlink() ================================================ FILE: hypothesis-python/src/hypothesis/extra/array_api.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys from collections.abc import Iterable, Iterator, Mapping, Sequence from numbers import Real from types import SimpleNamespace from typing import ( Any, Literal, NamedTuple, TypeAlias, TypeVar, get_args, ) from warnings import warn from weakref import WeakValueDictionary from hypothesis import strategies as st from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.extra._array_helpers import ( NDIM_MAX, BasicIndex, BasicIndexStrategy, BroadcastableShapes, Shape, array_shapes, broadcastable_shapes, check_argument, check_valid_dims, mutually_broadcastable_shapes as _mutually_broadcastable_shapes, order_check, valid_tuple_axes as _valid_tuple_axes, ) from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.coverage import check_function from hypothesis.internal.floats import next_down from hypothesis.internal.reflection import proxies from hypothesis.internal.validation import ( check_type, check_valid_bound, check_valid_integer, check_valid_interval, ) from hypothesis.strategies._internal.strategies import check_strategy from hypothesis.strategies._internal.utils import defines_strategy __all__ = [ "make_strategies_namespace", ] RELEASED_VERSIONS = ("2021.12", "2022.12", "2023.12", "2024.12", "2025.12") NOMINAL_VERSIONS = (*RELEASED_VERSIONS, "draft") assert sorted(NOMINAL_VERSIONS) == list(NOMINAL_VERSIONS) # sanity check NominalVersion = Literal["2021.12", "2022.12", "2023.12", "2024.12", "2025.12", "draft"] assert get_args(NominalVersion) == NOMINAL_VERSIONS # sanity check INT_NAMES = ("int8", "int16", "int32", "int64") UINT_NAMES = ("uint8", "uint16", "uint32", "uint64") ALL_INT_NAMES = INT_NAMES + UINT_NAMES FLOAT_NAMES = ("float32", "float64") REAL_NAMES = ALL_INT_NAMES + FLOAT_NAMES COMPLEX_NAMES = ("complex64", "complex128") NUMERIC_NAMES = REAL_NAMES + COMPLEX_NAMES DTYPE_NAMES = ("bool", *NUMERIC_NAMES) DataType = TypeVar("DataType") @check_function def check_xp_attributes(xp: Any, attributes: list[str]) -> None: missing_attrs = [attr for attr in attributes if not hasattr(xp, attr)] if len(missing_attrs) > 0: f_attrs = ", ".join(missing_attrs) raise InvalidArgument( f"Array module {xp.__name__} does not have required attributes: {f_attrs}" ) def partition_attributes_and_stubs( xp: Any, attributes: Iterable[str] ) -> tuple[list[Any], list[str]]: non_stubs = [] stubs = [] for attr in attributes: try: non_stubs.append(getattr(xp, attr)) except AttributeError: stubs.append(attr) return non_stubs, stubs def warn_on_missing_dtypes(xp: Any, stubs: list[str]) -> None: f_stubs = ", ".join(stubs) warn( f"Array module {xp.__name__} does not have the following " f"dtypes in its namespace: {f_stubs}", HypothesisWarning, stacklevel=3, ) def find_castable_builtin_for_dtype( xp: Any, api_version: NominalVersion, dtype: DataType ) -> type[bool | int | float | complex]: """Returns builtin type which can have values that are castable to the given dtype, according to :xp-ref:`type promotion rules `. For floating dtypes we always return ``float``, even though ``int`` is also castable. """ stubs = [] try: bool_dtype = xp.bool if dtype == bool_dtype: return bool except AttributeError: stubs.append("bool") int_dtypes, int_stubs = partition_attributes_and_stubs(xp, ALL_INT_NAMES) if dtype in int_dtypes: return int float_dtypes, float_stubs = partition_attributes_and_stubs(xp, FLOAT_NAMES) # None equals NumPy's xp.float64 object, so we specifically skip it here to # ensure that InvalidArgument is still raised. xp.float64 is in fact an # alias of np.dtype('float64'), and its equality with None is meant to be # deprecated at some point. See https://github.com/numpy/numpy/issues/18434 if dtype is not None and dtype in float_dtypes: return float stubs.extend(int_stubs) stubs.extend(float_stubs) if api_version > "2021.12": complex_dtypes, complex_stubs = partition_attributes_and_stubs( xp, COMPLEX_NAMES ) if dtype in complex_dtypes: return complex stubs.extend(complex_stubs) if len(stubs) > 0: warn_on_missing_dtypes(xp, stubs) raise InvalidArgument(f"dtype={dtype} not recognised in {xp.__name__}") @check_function def dtype_from_name(xp: Any, name: str) -> Any: if name in DTYPE_NAMES: try: return getattr(xp, name) except AttributeError as e: raise InvalidArgument( f"Array module {xp.__name__} does not have dtype {name} in its namespace" ) from e else: f_valid_dtypes = ", ".join(DTYPE_NAMES) raise InvalidArgument( f"{name} is not a valid Array API data type (pick from: {f_valid_dtypes})" ) def _from_dtype( xp: Any, api_version: NominalVersion, dtype: DataType | str, *, min_value: int | float | None = None, max_value: int | float | None = None, allow_nan: bool | None = None, allow_infinity: bool | None = None, allow_subnormal: bool | None = None, exclude_min: bool | None = None, exclude_max: bool | None = None, ) -> st.SearchStrategy[bool | int | float | complex]: """Return a strategy for any value of the given dtype. Values generated are of the Python scalar which is :xp-ref:`promotable ` to ``dtype``, where the values do not exceed its bounds. * ``dtype`` may be a dtype object or the string name of a :xp-ref:`valid dtype `. Compatible ``**kwargs`` are passed to the inferred strategy function for integers and floats. This allows you to customise the min and max values, and exclude non-finite numbers. This is particularly useful when kwargs are passed through from :func:`arrays()`, as it seamlessly handles the ``width`` or other representable bounds for you. """ # TODO: for next released xp version, add note for complex dtype support check_xp_attributes(xp, ["iinfo", "finfo"]) if isinstance(dtype, str): dtype = dtype_from_name(xp, dtype) builtin = find_castable_builtin_for_dtype(xp, api_version, dtype) def check_valid_minmax(prefix, val, info_obj): name = f"{prefix}_value" check_valid_bound(val, name) check_argument( val >= info_obj.min, f"dtype={dtype} requires {name}={val} to be at least {info_obj.min}", ) check_argument( val <= info_obj.max, f"dtype={dtype} requires {name}={val} to be at most {info_obj.max}", ) if builtin is bool: return st.booleans() elif builtin is int: iinfo = xp.iinfo(dtype) if min_value is None: min_value = iinfo.min if max_value is None: max_value = iinfo.max check_valid_integer(min_value, "min_value") check_valid_integer(max_value, "max_value") assert isinstance(min_value, int) assert isinstance(max_value, int) check_valid_minmax("min", min_value, iinfo) check_valid_minmax("max", max_value, iinfo) check_valid_interval(min_value, max_value, "min_value", "max_value") return st.integers(min_value=min_value, max_value=max_value) elif builtin is float: finfo = xp.finfo(dtype) kw = {} # Whilst we know the boundary values of float dtypes from finfo, we do # not assign them to the floats() strategy by default - passing min/max # values will modify test case reduction behaviour so that simple bugs # may become harder for users to identify. We plan to improve floats() # behaviour in https://github.com/HypothesisWorks/hypothesis/issues/2907. # Setting width should manage boundary values for us anyway. if min_value is not None: check_valid_bound(min_value, "min_value") assert isinstance(min_value, Real) check_valid_minmax("min", min_value, finfo) kw["min_value"] = min_value if max_value is not None: check_valid_bound(max_value, "max_value") assert isinstance(max_value, Real) check_valid_minmax("max", max_value, finfo) if min_value is not None: check_valid_interval(min_value, max_value, "min_value", "max_value") kw["max_value"] = max_value # We infer whether an array module will flush subnormals to zero, as may # be the case when libraries are built with compiler options that # violate IEEE-754 (e.g. -ffast-math and -ftz=true). Note we do this for # the specific dtype, as compilers may end up flushing subnormals for # one float but supporting subnormals for the other. # # By default, floats() will generate subnormals if they are in the # inferred values range. If we have detected that xp flushes to zero for # the passed dtype, we ensure from_dtype() will not generate subnormals # by default. if allow_subnormal is not None: kw["allow_subnormal"] = allow_subnormal else: subnormal = next_down(float(finfo.smallest_normal), width=finfo.bits) ftz = bool(xp.asarray(subnormal, dtype=dtype) == 0) if ftz: kw["allow_subnormal"] = False if allow_nan is not None: kw["allow_nan"] = allow_nan if allow_infinity is not None: kw["allow_infinity"] = allow_infinity if exclude_min is not None: kw["exclude_min"] = exclude_min if exclude_max is not None: kw["exclude_max"] = exclude_max return st.floats(width=finfo.bits, **kw) else: finfo = xp.finfo(dtype) # See above comment on FTZ inference. We explicitly infer with a # complex array, in case complex arrays have different FTZ behaviour # than arrays of the respective composite float. if allow_subnormal is None: subnormal = next_down(float(finfo.smallest_normal), width=finfo.bits) x = xp.asarray(complex(subnormal, subnormal), dtype=dtype) builtin_x = complex(x) allow_subnormal = builtin_x.real != 0 and builtin_x.imag != 0 return st.complex_numbers( allow_nan=allow_nan, allow_infinity=allow_infinity, allow_subnormal=allow_subnormal, width=finfo.bits * 2, ) class ArrayStrategy(st.SearchStrategy): def __init__( self, *, xp, api_version, elements_strategy, dtype, shape, fill, unique ): super().__init__() self.xp = xp self.elements_strategy = elements_strategy self.dtype = dtype self.shape = shape self.fill = fill self.unique = unique self.array_size = math.prod(shape) self.builtin = find_castable_builtin_for_dtype(xp, api_version, dtype) self.finfo = None if self.builtin is not float else xp.finfo(self.dtype) def check_set_value(self, val, val_0d, strategy): if val == val and self.builtin(val_0d) != val: if self.builtin is float: assert self.finfo is not None # for mypy try: is_subnormal = 0 < abs(val) < self.finfo.smallest_normal except Exception: # val may be a non-float that does not support the # operations __lt__ and __abs__ is_subnormal = False if is_subnormal: raise InvalidArgument( f"Generated subnormal float {val} from strategy " f"{strategy} resulted in {val_0d!r}, probably " f"as a result of array module {self.xp.__name__} " "being built with flush-to-zero compiler options. " "Consider passing allow_subnormal=False." ) raise InvalidArgument( f"Generated array element {val!r} from strategy {strategy} " f"cannot be represented with dtype {self.dtype}. " f"Array module {self.xp.__name__} instead " f"represents the element as {val_0d}. " "Consider using a more precise elements strategy, " "for example passing the width argument to floats()." ) def do_draw(self, data): if 0 in self.shape: return self.xp.zeros(self.shape, dtype=self.dtype) if self.fill.is_empty: # We have no fill value (either because the user explicitly # disabled it or because the default behaviour was used and our # elements strategy does not produce reusable values), so we must # generate a fully dense array with a freshly drawn value for each # entry. elems = data.draw( st.lists( self.elements_strategy, min_size=self.array_size, max_size=self.array_size, unique=self.unique, ) ) try: result = self.xp.asarray(elems, dtype=self.dtype) except Exception as e: if len(elems) <= 6: f_elems = str(elems) else: f_elems = f"[{elems[0]}, {elems[1]}, ..., {elems[-2]}, {elems[-1]}]" types = tuple( sorted({type(e) for e in elems}, key=lambda t: t.__name__) ) f_types = f"type {types[0]}" if len(types) == 1 else f"types {types}" raise InvalidArgument( f"Generated elements {f_elems} from strategy " f"{self.elements_strategy} could not be converted " f"to array of dtype {self.dtype}. " f"Consider if elements of {f_types} " f"are compatible with {self.dtype}." ) from e for i in range(self.array_size): self.check_set_value(elems[i], result[i], self.elements_strategy) else: # We draw arrays as "sparse with an offset". We assume not every # element will be assigned and so first draw a single value from our # fill strategy to create a full array. We then draw a collection of # index assignments within the array and assign fresh values from # our elements strategy to those indices. fill_val = data.draw(self.fill) result_obj = [fill_val for _ in range(self.array_size)] fill_mask = [True for _ in range(self.array_size)] elements = cu.many( data, min_size=0, max_size=self.array_size, # sqrt isn't chosen for any particularly principled reason. It # just grows reasonably quickly but sublinearly, and for small # arrays it represents a decent fraction of the array size. average_size=min( 0.9 * self.array_size, # ensure small arrays sometimes use fill max(10, math.sqrt(self.array_size)), # ...but *only* sometimes ), ) assigned = set() seen = set() while elements.more(): i = data.draw_integer(0, self.array_size - 1) if i in assigned: elements.reject("chose an array index we've already used") continue val = data.draw(self.elements_strategy) if self.unique: if val in seen: elements.reject("chose an element we've already used") continue seen.add(val) result_obj[i] = val assigned.add(i) fill_mask[i] = False try: result = self.xp.asarray(result_obj, dtype=self.dtype) except Exception as e: f_expr = f"xp.asarray({result_obj}, dtype={self.dtype})" raise InvalidArgument(f"Could not create array via {f_expr}") from e for i, val in enumerate(result_obj): val_0d = result[i] if fill_mask[i] and self.unique: if not self.xp.isnan(val_0d): raise InvalidArgument( f"Array module {self.xp.__name__} did not recognise fill " f"value {fill_val!r} as NaN - instead got {val_0d!r}. " "Cannot fill unique array with non-NaN values." ) else: self.check_set_value(val, val_0d, self.elements_strategy) return self.xp.reshape(result, self.shape) def _arrays( xp: Any, api_version: NominalVersion, dtype: DataType | str | st.SearchStrategy[DataType] | st.SearchStrategy[str], shape: int | Shape | st.SearchStrategy[Shape], *, elements: Mapping[str, Any] | st.SearchStrategy | None = None, fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> st.SearchStrategy: """Returns a strategy for :xp-ref:`arrays `. * ``dtype`` may be a :xp-ref:`valid dtype ` object or name, or a strategy that generates such values. * ``shape`` may be an integer >= 0, a tuple of such integers, or a strategy that generates such values. * ``elements`` is a strategy for values to put in the array. If ``None`` then a suitable value will be inferred based on the dtype, which may give any legal value (including e.g. NaN for floats). If a mapping, it will be passed as ``**kwargs`` to :func:`from_dtype()` when inferring based on the dtype. * ``fill`` is a strategy that may be used to generate a single background value for the array. If ``None``, a suitable default will be inferred based on the other arguments. If set to :func:`~hypothesis.strategies.nothing` then filling behaviour will be disabled entirely and every element will be generated independently. * ``unique`` specifies if the elements of the array should all be distinct from one another; if fill is also set, the only valid values for fill to return are NaN values. Arrays of specified ``dtype`` and ``shape`` are generated for example like this: .. code-block:: pycon >>> from numpy import array_api as xp >>> xps.arrays(xp, xp.int8, (2, 3)).example() Array([[-8, 6, 3], [-6, 4, 6]], dtype=int8) Specifying element boundaries by a :obj:`python:dict` of the kwargs to pass to :func:`from_dtype` will ensure ``dtype`` bounds will be respected. .. code-block:: pycon >>> xps.arrays(xp, xp.int8, 3, elements={"min_value": 10}).example() Array([125, 13, 79], dtype=int8) .. code-block:: pycon >>> xps.arrays(xp, xp.float32, 3, elements=floats(0, 1, width=32)).example() Array([ 0.88974794, 0.77387938, 0.1977879 ], dtype=float32) Array values are generated in two parts: 1. A single value is drawn from the fill strategy and is used to create a filled array. 2. Some subset of the coordinates of the array are populated with a value drawn from the elements strategy (or its inferred form). You can set ``fill`` to :func:`~hypothesis.strategies.nothing` if you want to disable this behaviour and draw a value for every element. By default ``arrays`` will attempt to infer the correct fill behaviour: if ``unique`` is also ``True``, no filling will occur. Otherwise, if it looks safe to reuse the values of elements across multiple coordinates (this will be the case for any inferred strategy, and for most of the builtins, but is not the case for mutable values or strategies built with flatmap, map, composite, etc.) then it will use the elements strategy as the fill, else it will default to having no fill. Having a fill helps Hypothesis craft high quality examples, but its main importance is when the array generated is large: Hypothesis is primarily designed around testing small examples. If you have arrays with hundreds or more elements, having a fill value is essential if you want your tests to run in reasonable time. """ check_xp_attributes( xp, ["finfo", "asarray", "zeros", "all", "isnan", "isfinite", "reshape"] ) if isinstance(dtype, st.SearchStrategy): return dtype.flatmap( lambda d: _arrays( xp, api_version, d, shape, elements=elements, fill=fill, unique=unique ) ) elif isinstance(dtype, str): dtype = dtype_from_name(xp, dtype) if isinstance(shape, st.SearchStrategy): return shape.flatmap( lambda s: _arrays( xp, api_version, dtype, s, elements=elements, fill=fill, unique=unique ) ) elif isinstance(shape, int): shape = (shape,) elif not isinstance(shape, tuple): raise InvalidArgument(f"shape={shape} is not a valid shape or strategy") check_argument( all(isinstance(x, int) and x >= 0 for x in shape), f"{shape=}, but all dimensions must be non-negative integers.", ) if elements is None: elements = _from_dtype(xp, api_version, dtype) elif isinstance(elements, Mapping): elements = _from_dtype(xp, api_version, dtype, **elements) check_strategy(elements, "elements") if fill is None: assert isinstance(elements, st.SearchStrategy) # for mypy if unique or not elements.has_reusable_values: fill = st.nothing() else: fill = elements check_strategy(fill, "fill") return ArrayStrategy( xp=xp, api_version=api_version, elements_strategy=elements, dtype=dtype, shape=shape, fill=fill, unique=unique, ) @check_function def check_dtypes(xp: Any, dtypes: list[DataType], stubs: list[str]) -> None: if len(dtypes) == 0: assert len(stubs) > 0, "No dtypes passed but stubs is empty" f_stubs = ", ".join(stubs) raise InvalidArgument( f"Array module {xp.__name__} does not have the following " f"required dtypes in its namespace: {f_stubs}" ) elif len(stubs) > 0: warn_on_missing_dtypes(xp, stubs) def _scalar_dtypes(xp: Any, api_version: NominalVersion) -> st.SearchStrategy[DataType]: """Return a strategy for all :xp-ref:`valid dtype ` objects.""" return st.one_of(_boolean_dtypes(xp), _numeric_dtypes(xp, api_version)) def _boolean_dtypes(xp: Any) -> st.SearchStrategy[DataType]: """Return a strategy for just the boolean dtype object.""" try: return st.just(xp.bool) except AttributeError: raise InvalidArgument( f"Array module {xp.__name__} does not have a bool dtype in its namespace" ) from None def _real_dtypes(xp: Any) -> st.SearchStrategy[DataType]: """Return a strategy for all real-valued dtype objects.""" return st.one_of( _integer_dtypes(xp), _unsigned_integer_dtypes(xp), _floating_dtypes(xp), ) def _numeric_dtypes( xp: Any, api_version: NominalVersion ) -> st.SearchStrategy[DataType]: """Return a strategy for all numeric dtype objects.""" strat: st.SearchStrategy[DataType] = _real_dtypes(xp) if api_version > "2021.12": strat |= _complex_dtypes(xp) return strat @check_function def check_valid_sizes( category: str, sizes: Sequence[int], valid_sizes: Sequence[int] ) -> None: check_argument(len(sizes) > 0, "No sizes passed") invalid_sizes = [s for s in sizes if s not in valid_sizes] f_valid_sizes = ", ".join(str(s) for s in valid_sizes) f_invalid_sizes = ", ".join(str(s) for s in invalid_sizes) check_argument( len(invalid_sizes) == 0, f"The following sizes are not valid for {category} dtypes: " f"{f_invalid_sizes} (valid sizes: {f_valid_sizes})", ) def numeric_dtype_names(base_name: str, sizes: Sequence[int]) -> Iterator[str]: for size in sizes: yield f"{base_name}{size}" IntSize: TypeAlias = Literal[8, 16, 32, 64] FltSize: TypeAlias = Literal[32, 64] CpxSize: TypeAlias = Literal[64, 128] def _integer_dtypes( xp: Any, *, sizes: IntSize | Sequence[IntSize] = (8, 16, 32, 64) ) -> st.SearchStrategy[DataType]: """Return a strategy for signed integer dtype objects. ``sizes`` contains the signed integer sizes in bits, defaulting to ``(8, 16, 32, 64)`` which covers all valid sizes. """ if isinstance(sizes, int): sizes = (sizes,) check_valid_sizes("int", sizes, (8, 16, 32, 64)) dtypes, stubs = partition_attributes_and_stubs( xp, numeric_dtype_names("int", sizes) ) check_dtypes(xp, dtypes, stubs) return st.sampled_from(dtypes) def _unsigned_integer_dtypes( xp: Any, *, sizes: IntSize | Sequence[IntSize] = (8, 16, 32, 64) ) -> st.SearchStrategy[DataType]: """Return a strategy for unsigned integer dtype objects. ``sizes`` contains the unsigned integer sizes in bits, defaulting to ``(8, 16, 32, 64)`` which covers all valid sizes. """ if isinstance(sizes, int): sizes = (sizes,) check_valid_sizes("int", sizes, (8, 16, 32, 64)) dtypes, stubs = partition_attributes_and_stubs( xp, numeric_dtype_names("uint", sizes) ) check_dtypes(xp, dtypes, stubs) return st.sampled_from(dtypes) def _floating_dtypes( xp: Any, *, sizes: FltSize | Sequence[FltSize] = (32, 64) ) -> st.SearchStrategy[DataType]: """Return a strategy for real-valued floating-point dtype objects. ``sizes`` contains the floating-point sizes in bits, defaulting to ``(32, 64)`` which covers all valid sizes. """ if isinstance(sizes, int): sizes = (sizes,) check_valid_sizes("int", sizes, (32, 64)) dtypes, stubs = partition_attributes_and_stubs( xp, numeric_dtype_names("float", sizes) ) check_dtypes(xp, dtypes, stubs) return st.sampled_from(dtypes) def _complex_dtypes( xp: Any, *, sizes: CpxSize | Sequence[CpxSize] = (64, 128) ) -> st.SearchStrategy[DataType]: """Return a strategy for complex dtype objects. ``sizes`` contains the complex sizes in bits, defaulting to ``(64, 128)`` which covers all valid sizes. """ if isinstance(sizes, int): sizes = (sizes,) check_valid_sizes("complex", sizes, (64, 128)) dtypes, stubs = partition_attributes_and_stubs( xp, numeric_dtype_names("complex", sizes) ) check_dtypes(xp, dtypes, stubs) return st.sampled_from(dtypes) @proxies(_valid_tuple_axes) def valid_tuple_axes(*args, **kwargs): return _valid_tuple_axes(*args, **kwargs) valid_tuple_axes.__doc__ = f""" Return a strategy for permissible tuple-values for the ``axis`` argument in Array API sequential methods e.g. ``sum``, given the specified dimensionality. {_valid_tuple_axes.__doc__} """ @defines_strategy() def mutually_broadcastable_shapes( num_shapes: int, *, base_shape: Shape = (), min_dims: int = 0, max_dims: int | None = None, min_side: int = 1, max_side: int | None = None, ) -> st.SearchStrategy[BroadcastableShapes]: return _mutually_broadcastable_shapes( num_shapes=num_shapes, base_shape=base_shape, min_dims=min_dims, max_dims=max_dims, min_side=min_side, max_side=max_side, ) mutually_broadcastable_shapes.__doc__ = _mutually_broadcastable_shapes.__doc__ @defines_strategy() def indices( shape: Shape, *, min_dims: int = 0, max_dims: int | None = None, allow_newaxis: bool = False, allow_ellipsis: bool = True, ) -> st.SearchStrategy[BasicIndex]: """Return a strategy for :xp-ref:`valid indices ` of arrays with the specified shape, which may include dimensions of size zero. It generates tuples containing some mix of integers, :obj:`python:slice` objects, ``...`` (an ``Ellipsis``), and ``None``. When a length-one tuple would be generated, this strategy may instead return the element which will index the first axis, e.g. ``5`` instead of ``(5,)``. * ``shape`` is the shape of the array that will be indexed, as a tuple of integers >= 0. This must be at least two-dimensional for a tuple to be a valid index; for one-dimensional arrays use :func:`~hypothesis.strategies.slices` instead. * ``min_dims`` is the minimum dimensionality of the resulting array from use of the generated index. * ``max_dims`` is the maximum dimensionality of the resulting array, defaulting to ``len(shape) if not allow_newaxis else max(len(shape), min_dims) + 2``. * ``allow_ellipsis`` specifies whether ``None`` is allowed in the index. * ``allow_ellipsis`` specifies whether ``...`` is allowed in the index. """ check_type(tuple, shape, "shape") check_argument( all(isinstance(x, int) and x >= 0 for x in shape), f"{shape=}, but all dimensions must be non-negative integers.", ) check_type(bool, allow_newaxis, "allow_newaxis") check_type(bool, allow_ellipsis, "allow_ellipsis") check_type(int, min_dims, "min_dims") if not allow_newaxis: check_argument( min_dims <= len(shape), f"min_dims={min_dims} is larger than len(shape)={len(shape)}, " "but it is impossible for an indexing operation to add dimensions ", "when allow_newaxis=False.", ) check_valid_dims(min_dims, "min_dims") if max_dims is None: if allow_newaxis: max_dims = min(max(len(shape), min_dims) + 2, NDIM_MAX) else: max_dims = min(len(shape), NDIM_MAX) check_type(int, max_dims, "max_dims") assert isinstance(max_dims, int) if not allow_newaxis: check_argument( max_dims <= len(shape), f"max_dims={max_dims} is larger than len(shape)={len(shape)}, " "but it is impossible for an indexing operation to add dimensions ", "when allow_newaxis=False.", ) check_valid_dims(max_dims, "max_dims") order_check("dims", 0, min_dims, max_dims) return BasicIndexStrategy( shape, min_dims=min_dims, max_dims=max_dims, allow_ellipsis=allow_ellipsis, allow_newaxis=allow_newaxis, allow_fewer_indices_than_dims=False, ) # Cache for make_strategies_namespace() _args_to_xps: WeakValueDictionary = WeakValueDictionary() def make_strategies_namespace( xp: Any, *, api_version: NominalVersion | None = None ) -> SimpleNamespace: """Creates a strategies namespace for the given array module. * ``xp`` is the Array API library to automatically pass to the namespaced methods. * ``api_version`` is the version of the Array API which the returned strategies namespace should conform to. If ``None``, the latest API version which ``xp`` supports will be inferred from ``xp.__array_api_version__``. If a version string in the ``YYYY.MM`` format, the strategies namespace will conform to that version if supported. A :obj:`python:types.SimpleNamespace` is returned which contains all the strategy methods in this module but without requiring the ``xp`` argument. Creating and using a strategies namespace for NumPy's Array API implementation would go like this: .. code-block:: pycon >>> xp.__array_api_version__ # xp is your desired array library '2021.12' >>> xps = make_strategies_namespace(xp) >>> xps.api_version '2021.12' >>> x = xps.arrays(xp.int8, (2, 3)).example() >>> x Array([[-8, 6, 3], [-6, 4, 6]], dtype=int8) >>> x.__array_namespace__() is xp True """ not_available_msg = ( "If the standard version you want is not available, please ensure " "you're using the latest version of Hypothesis, then open an issue if " "one doesn't already exist." ) if api_version is None: check_argument( hasattr(xp, "__array_api_version__"), f"Array module {xp.__name__} has no attribute __array_api_version__, " "which is required when inferring api_version. If you believe " f"{xp.__name__} is indeed an Array API module, try explicitly " "passing an api_version.", ) check_argument( isinstance(xp.__array_api_version__, str) and xp.__array_api_version__ in RELEASED_VERSIONS, f"{xp.__array_api_version__=}, but it must " f"be a valid version string {RELEASED_VERSIONS}. {not_available_msg}", ) api_version = xp.__array_api_version__ inferred_version = True else: check_argument( isinstance(api_version, str) and api_version in NOMINAL_VERSIONS, f"{api_version=}, but it must be None, or a valid version " f"string in {RELEASED_VERSIONS}. {not_available_msg}", ) inferred_version = False try: array = xp.zeros(1) array.__array_namespace__() except Exception: warn( f"Could not determine whether module {xp.__name__} is an Array API library", HypothesisWarning, stacklevel=2, ) try: namespace = _args_to_xps[(xp, api_version)] except (KeyError, TypeError): pass else: return namespace @defines_strategy(force_reusable_values=True) def from_dtype( dtype: DataType | str, *, min_value: int | float | None = None, max_value: int | float | None = None, allow_nan: bool | None = None, allow_infinity: bool | None = None, allow_subnormal: bool | None = None, exclude_min: bool | None = None, exclude_max: bool | None = None, ) -> st.SearchStrategy[bool | int | float | complex]: return _from_dtype( xp, api_version, dtype, min_value=min_value, max_value=max_value, allow_nan=allow_nan, allow_infinity=allow_infinity, allow_subnormal=allow_subnormal, exclude_min=exclude_min, exclude_max=exclude_max, ) @defines_strategy(force_reusable_values=True) def arrays( dtype: DataType | str | st.SearchStrategy[DataType] | st.SearchStrategy[str], shape: int | Shape | st.SearchStrategy[Shape], *, elements: Mapping[str, Any] | st.SearchStrategy | None = None, fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> st.SearchStrategy: return _arrays( xp, api_version, dtype, shape, elements=elements, fill=fill, unique=unique, ) @defines_strategy() def scalar_dtypes() -> st.SearchStrategy[DataType]: return _scalar_dtypes(xp, api_version) @defines_strategy() def boolean_dtypes() -> st.SearchStrategy[DataType]: return _boolean_dtypes(xp) @defines_strategy() def real_dtypes() -> st.SearchStrategy[DataType]: return _real_dtypes(xp) @defines_strategy() def numeric_dtypes() -> st.SearchStrategy[DataType]: return _numeric_dtypes(xp, api_version) @defines_strategy() def integer_dtypes( *, sizes: IntSize | Sequence[IntSize] = (8, 16, 32, 64) ) -> st.SearchStrategy[DataType]: return _integer_dtypes(xp, sizes=sizes) @defines_strategy() def unsigned_integer_dtypes( *, sizes: IntSize | Sequence[IntSize] = (8, 16, 32, 64) ) -> st.SearchStrategy[DataType]: return _unsigned_integer_dtypes(xp, sizes=sizes) @defines_strategy() def floating_dtypes( *, sizes: FltSize | Sequence[FltSize] = (32, 64) ) -> st.SearchStrategy[DataType]: return _floating_dtypes(xp, sizes=sizes) from_dtype.__doc__ = _from_dtype.__doc__ arrays.__doc__ = _arrays.__doc__ scalar_dtypes.__doc__ = _scalar_dtypes.__doc__ boolean_dtypes.__doc__ = _boolean_dtypes.__doc__ real_dtypes.__doc__ = _real_dtypes.__doc__ numeric_dtypes.__doc__ = _numeric_dtypes.__doc__ integer_dtypes.__doc__ = _integer_dtypes.__doc__ unsigned_integer_dtypes.__doc__ = _unsigned_integer_dtypes.__doc__ floating_dtypes.__doc__ = _floating_dtypes.__doc__ class StrategiesNamespace(SimpleNamespace): def __init__(self, **kwargs): for attr in ["name", "api_version"]: if attr not in kwargs: raise ValueError(f"'{attr}' kwarg required") super().__init__(**kwargs) @property def complex_dtypes(self): try: return self.__dict__["complex_dtypes"] except KeyError as e: raise AttributeError( "You attempted to access 'complex_dtypes', but it is not " f"available for api_version='{self.api_version}' of " f"xp={self.name}." ) from e def __repr__(self): f_args = self.name if not inferred_version: f_args += f", api_version='{self.api_version}'" return f"make_strategies_namespace({f_args})" kwargs = { "name": xp.__name__, "api_version": api_version, "from_dtype": from_dtype, "arrays": arrays, "array_shapes": array_shapes, "scalar_dtypes": scalar_dtypes, "boolean_dtypes": boolean_dtypes, "real_dtypes": real_dtypes, "numeric_dtypes": numeric_dtypes, "integer_dtypes": integer_dtypes, "unsigned_integer_dtypes": unsigned_integer_dtypes, "floating_dtypes": floating_dtypes, "valid_tuple_axes": valid_tuple_axes, "broadcastable_shapes": broadcastable_shapes, "mutually_broadcastable_shapes": mutually_broadcastable_shapes, "indices": indices, } if api_version > "2021.12": @defines_strategy() def complex_dtypes( *, sizes: CpxSize | Sequence[CpxSize] = (64, 128) ) -> st.SearchStrategy[DataType]: return _complex_dtypes(xp, sizes=sizes) complex_dtypes.__doc__ = _complex_dtypes.__doc__ kwargs["complex_dtypes"] = complex_dtypes namespace = StrategiesNamespace(**kwargs) try: _args_to_xps[(xp, api_version)] = namespace except TypeError: pass return namespace try: import numpy as np except ImportError: if "sphinx" in sys.modules: # This is pretty awkward, but also the best way available from unittest.mock import Mock np = Mock() else: np = None # type: ignore[assignment] if np is not None: class FloatInfo(NamedTuple): bits: int eps: float max: float min: float smallest_normal: float def mock_finfo(dtype: DataType) -> FloatInfo: """Returns a finfo object compliant with the Array API Ensures all attributes are Python scalars and not NumPy scalars. This lets us ignore corner cases with how NumPy scalars operate, such as NumPy floats breaking our next_down() util. Also ensures the finfo obj has the smallest_normal attribute. NumPy only introduced it in v1.21.1, so we just use the equivalent tiny attribute to keep mocking with older versions working. """ _finfo = np.finfo(dtype) # type: ignore[call-overload] return FloatInfo( int(_finfo.bits), float(_finfo.eps), float(_finfo.max), float(_finfo.min), float(_finfo.tiny), ) mock_xp = SimpleNamespace( __name__="mock", __array_api_version__="2022.12", # Data types int8=np.int8, int16=np.int16, int32=np.int32, int64=np.int64, uint8=np.uint8, uint16=np.uint16, uint32=np.uint32, uint64=np.uint64, float32=np.float32, float64=np.float64, complex64=np.complex64, complex128=np.complex128, bool=np.bool_, # Constants nan=np.nan, # Data type functions astype=lambda x, d: x.astype(d), iinfo=np.iinfo, finfo=mock_finfo, broadcast_arrays=np.broadcast_arrays, # Creation functions arange=np.arange, asarray=np.asarray, empty=np.empty, zeros=np.zeros, ones=np.ones, # Manipulation functions reshape=np.reshape, # Element-wise functions isnan=np.isnan, isfinite=np.isfinite, logical_or=np.logical_or, # Statistical functions sum=np.sum, # Searching functions nonzero=np.nonzero, # Sorting functions sort=np.sort, # Set functions unique_values=np.unique, # Utility functions any=np.any, all=np.all, ) ================================================ FILE: hypothesis-python/src/hypothesis/extra/cli.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ :: $ hypothesis --help Usage: hypothesis [OPTIONS] COMMAND [ARGS]... Options: --version Show the version and exit. -h, --help Show this message and exit. Commands: codemod `hypothesis codemod` refactors deprecated or inefficient code. fuzz [hypofuzz] runs tests with an adaptive coverage-guided fuzzer. write `hypothesis write` writes property-based tests for you! This module requires the :pypi:`click` package, and provides Hypothesis' command-line interface, for e.g. :ref:`'ghostwriting' tests ` via the terminal. It's also where `HypoFuzz `__ adds the :command:`hypothesis fuzz` command (`learn more about that here `__). """ import builtins import importlib import inspect import sys import types from difflib import get_close_matches from functools import partial from multiprocessing import Pool from pathlib import Path try: import pytest except ImportError: pytest = None # type: ignore MESSAGE = """ The Hypothesis command-line interface requires the `{}` package, which you do not have installed. Run: python -m pip install --upgrade 'hypothesis[cli]' and try again. """ try: import click except ImportError: def main(): """If `click` is not installed, tell the user to install it then exit.""" sys.stderr.write(MESSAGE.format("click")) sys.exit(1) else: # Ensure that Python scripts in the current working directory are importable, # on the principle that Ghostwriter should 'just work' for novice users. Note # that we append rather than prepend to the module search path, so this will # never shadow the stdlib or installed packages. sys.path.append(".") @click.group(context_settings={"help_option_names": ("-h", "--help")}) @click.version_option() def main(): pass def obj_name(s: str) -> object: """This "type" imports whatever object is named by a dotted string.""" s = s.strip() if "/" in s or "\\" in s: raise click.UsageError( "Remember that the ghostwriter should be passed the name of a module, not a path." ) from None try: return importlib.import_module(s) except ImportError: pass classname = None if "." not in s: modulename, module, funcname = "builtins", builtins, s else: modulename, funcname = s.rsplit(".", 1) try: module = importlib.import_module(modulename) except ImportError as err: try: modulename, classname = modulename.rsplit(".", 1) module = importlib.import_module(modulename) except (ImportError, ValueError): if s.endswith(".py"): raise click.UsageError( "Remember that the ghostwriter should be passed the name of a module, not a file." ) from None raise click.UsageError( f"Failed to import the {modulename} module for introspection. " "Check spelling and your Python import path, or use the Python API?" ) from err def describe_close_matches( module_or_class: types.ModuleType, objname: str ) -> str: public_names = [ name for name in vars(module_or_class) if not name.startswith("_") ] matches = get_close_matches(objname, public_names) if matches: return f" Closest matches: {matches!r}" else: return "" if classname is None: try: return getattr(module, funcname) except AttributeError as err: if funcname == "py": # Likely attempted to pass a local file (Eg., "myscript.py") instead of a module name raise click.UsageError( "Remember that the ghostwriter should be passed the name of a module, not a file." f"\n\tTry: hypothesis write {s[:-3]}" ) from None raise click.UsageError( f"Found the {modulename!r} module, but it doesn't have a " f"{funcname!r} attribute." + describe_close_matches(module, funcname) ) from err else: try: func_class = getattr(module, classname) except AttributeError as err: raise click.UsageError( f"Found the {modulename!r} module, but it doesn't have a " f"{classname!r} class." + describe_close_matches(module, classname) ) from err try: return getattr(func_class, funcname) except AttributeError as err: if inspect.isclass(func_class): func_class_is = "class" else: func_class_is = "attribute" raise click.UsageError( f"Found the {modulename!r} module and {classname!r} {func_class_is}, " f"but it doesn't have a {funcname!r} attribute." + describe_close_matches(func_class, funcname) ) from err def _refactor(func, fname): try: oldcode = Path(fname).read_text(encoding="utf-8") except (OSError, UnicodeError) as err: # Permissions or encoding issue, or file deleted, etc. return f"skipping {fname!r} due to {err}" if "hypothesis" not in oldcode: return # This is a fast way to avoid running slow no-op codemods try: newcode = func(oldcode) except Exception as err: from libcst import ParserSyntaxError if isinstance(err, ParserSyntaxError): from hypothesis.extra._patching import indent msg = indent(str(err).replace("\n\n", "\n"), " ").strip() return f"skipping {fname!r} due to {msg}" raise if newcode != oldcode: Path(fname).write_text(newcode, encoding="utf-8") @main.command() # type: ignore # Click adds the .command attribute @click.argument("path", type=str, required=True, nargs=-1) def codemod(path): """`hypothesis codemod` refactors deprecated or inefficient code. It adapts `python -m libcst.tool`, removing many features and config options which are rarely relevant for this purpose. If you need more control, we encourage you to use the libcst CLI directly; if not this one is easier. PATH is the file(s) or directories of files to format in place, or "-" to read from stdin and write to stdout. """ try: from libcst.codemod import gather_files from hypothesis.extra import codemods except ImportError: sys.stderr.write( "You are missing required dependencies for this option. Run:\n\n" " python -m pip install --upgrade hypothesis[codemods]\n\n" "and try again." ) sys.exit(1) # Special case for stdin/stdout usage if "-" in path: if len(path) > 1: raise Exception( "Cannot specify multiple paths when reading from stdin!" ) print("Codemodding from stdin", file=sys.stderr) print(codemods.refactor(sys.stdin.read())) return 0 # Find all the files to refactor, and then codemod them files = gather_files(path) errors = set() if len(files) <= 1: errors.add(_refactor(codemods.refactor, *files)) else: with Pool() as pool: for msg in pool.imap_unordered( partial(_refactor, codemods.refactor), files ): errors.add(msg) errors.discard(None) for msg in errors: print(msg, file=sys.stderr) return 1 if errors else 0 @main.command() # type: ignore # Click adds the .command attribute @click.argument("func", type=obj_name, required=True, nargs=-1) @click.option( "--roundtrip", "writer", flag_value="roundtrip", help="start by testing write/read or encode/decode!", ) @click.option( "--equivalent", "writer", flag_value="equivalent", help="very useful when optimising or refactoring code", ) @click.option( "--errors-equivalent", "writer", flag_value="errors-equivalent", help="--equivalent, but also allows consistent errors", ) @click.option( "--idempotent", "writer", flag_value="idempotent", help="check that f(x) == f(f(x))", ) @click.option( "--binary-op", "writer", flag_value="binary_operation", help="associativity, commutativity, identity element", ) # Note: we deliberately omit a --ufunc flag, because the magic() # detection of ufuncs is both precise and complete. @click.option( "--style", type=click.Choice(["pytest", "unittest"]), default="pytest" if pytest else "unittest", help="pytest-style function, or unittest-style method?", ) @click.option( "-e", "--except", "except_", type=obj_name, multiple=True, help="dotted name of exception(s) to ignore", ) @click.option( "--annotate/--no-annotate", default=None, help="force ghostwritten tests to be type-annotated (or not). " "By default, match the code to test.", ) def write(func, writer, except_, style, annotate): # \b disables autowrap """`hypothesis write` writes property-based tests for you! Type annotations are helpful but not required for our advanced introspection and templating logic. Try running the examples below to see how it works: \b hypothesis write gzip hypothesis write numpy.matmul hypothesis write pandas.from_dummies hypothesis write re.compile --except re.error hypothesis write --equivalent ast.literal_eval eval hypothesis write --roundtrip json.dumps json.loads hypothesis write --style=unittest --idempotent sorted hypothesis write --binary-op operator.add """ # NOTE: if you want to call this function from Python, look instead at the # ``hypothesis.extra.ghostwriter`` module. Click-decorated functions have # a different calling convention, and raise SystemExit instead of returning. kwargs = {"except_": except_ or (), "style": style, "annotate": annotate} if writer is None: writer = "magic" elif writer == "idempotent" and len(func) > 1: raise click.UsageError("Test functions for idempotence one at a time.") elif writer == "roundtrip" and len(func) == 1: writer = "idempotent" elif "equivalent" in writer and len(func) == 1: writer = "fuzz" if writer == "errors-equivalent": writer = "equivalent" kwargs["allow_same_errors"] = True try: from hypothesis.extra import ghostwriter except ImportError: sys.stderr.write(MESSAGE.format("black")) sys.exit(1) code = getattr(ghostwriter, writer)(*func, **kwargs) try: from rich.console import Console from rich.syntax import Syntax from hypothesis.utils.terminal import guess_background_color except ImportError: print(code) else: try: theme = "default" if guess_background_color() == "light" else "monokai" code = Syntax(code, "python", background_color="default", theme=theme) Console().print(code, soft_wrap=True) except Exception: print("# Error while syntax-highlighting code", file=sys.stderr) print(code) ================================================ FILE: hypothesis-python/src/hypothesis/extra/codemods.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ This module provides codemods based on the :pypi:`LibCST` library, which can both detect *and automatically fix* issues with code that uses Hypothesis, including upgrading from deprecated features to our recommended style. You can run the codemods via our CLI:: $ hypothesis codemod --help Usage: hypothesis codemod [OPTIONS] PATH... `hypothesis codemod` refactors deprecated or inefficient code. It adapts `python -m libcst.tool`, removing many features and config options which are rarely relevant for this purpose. If you need more control, we encourage you to use the libcst CLI directly; if not this one is easier. PATH is the file(s) or directories of files to format in place, or "-" to read from stdin and write to stdout. Options: -h, --help Show this message and exit. Alternatively you can use ``python -m libcst.tool``, which offers more control at the cost of additional configuration (adding ``'hypothesis.extra'`` to the ``modules`` list in ``.libcst.codemod.yaml``) and `some issues on Windows `__. .. autofunction:: refactor """ import functools import importlib from inspect import Parameter, signature from typing import ClassVar import libcst as cst import libcst.matchers as m from libcst.codemod import VisitorBasedCodemodCommand def refactor(code: str) -> str: """Update a source code string from deprecated to modern Hypothesis APIs. This may not fix *all* the deprecation warnings in your code, but we're confident that it will be easier than doing it all by hand. We recommend using the CLI, but if you want a Python function here it is. """ context = cst.codemod.CodemodContext() mod = cst.parse_module(code) transforms: list[VisitorBasedCodemodCommand] = [ HypothesisFixPositionalKeywonlyArgs(context), HypothesisFixComplexMinMagnitude(context), HypothesisFixHealthCheckAll(context), HypothesisFixCharactersArguments(context), ] for transform in transforms: mod = transform.transform_module(mod) return mod.code def match_qualname(name): # We use the metadata to get qualname instead of matching directly on function # name, because this handles some scope and "from x import y as z" issues. return m.MatchMetadataIfTrue( cst.metadata.QualifiedNameProvider, # If there are multiple possible qualnames, e.g. due to conditional imports, # be conservative. Better to leave the user to fix a few things by hand than # to break their code while attempting to refactor it! lambda qualnames: all(n.name == name for n in qualnames), ) class HypothesisFixComplexMinMagnitude(VisitorBasedCodemodCommand): """Fix a deprecated min_magnitude=None argument for complex numbers:: st.complex_numbers(min_magnitude=None) -> st.complex_numbers(min_magnitude=0) Note that this should be run *after* ``HypothesisFixPositionalKeywonlyArgs``, in order to handle ``st.complex_numbers(None)``. """ DESCRIPTION = "Fix a deprecated min_magnitude=None argument for complex numbers." METADATA_DEPENDENCIES = (cst.metadata.QualifiedNameProvider,) @m.call_if_inside( m.Call(metadata=match_qualname("hypothesis.strategies.complex_numbers")) ) def leave_Arg(self, original_node, updated_node): if m.matches( updated_node, m.Arg(keyword=m.Name("min_magnitude"), value=m.Name("None")) ): return updated_node.with_changes(value=cst.Integer("0")) return updated_node @functools.lru_cache def get_fn(import_path): mod, fn = import_path.rsplit(".", 1) return getattr(importlib.import_module(mod), fn) class HypothesisFixPositionalKeywonlyArgs(VisitorBasedCodemodCommand): """Fix positional arguments for newly keyword-only parameters, e.g.:: st.fractions(0, 1, 9) -> st.fractions(0, 1, max_denominator=9) Applies to a majority of our public API, since keyword-only parameters are great but we couldn't use them until after we dropped support for Python 2. """ DESCRIPTION = "Fix positional arguments for newly keyword-only parameters." METADATA_DEPENDENCIES = (cst.metadata.QualifiedNameProvider,) kwonly_functions = ( "hypothesis.target", "hypothesis.find", "hypothesis.extra.lark.from_lark", "hypothesis.extra.numpy.arrays", "hypothesis.extra.numpy.array_shapes", "hypothesis.extra.numpy.unsigned_integer_dtypes", "hypothesis.extra.numpy.integer_dtypes", "hypothesis.extra.numpy.floating_dtypes", "hypothesis.extra.numpy.complex_number_dtypes", "hypothesis.extra.numpy.datetime64_dtypes", "hypothesis.extra.numpy.timedelta64_dtypes", "hypothesis.extra.numpy.byte_string_dtypes", "hypothesis.extra.numpy.unicode_string_dtypes", "hypothesis.extra.numpy.array_dtypes", "hypothesis.extra.numpy.nested_dtypes", "hypothesis.extra.numpy.valid_tuple_axes", "hypothesis.extra.numpy.broadcastable_shapes", "hypothesis.extra.pandas.indexes", "hypothesis.extra.pandas.series", "hypothesis.extra.pandas.columns", "hypothesis.extra.pandas.data_frames", "hypothesis.provisional.domains", "hypothesis.stateful.run_state_machine_as_test", "hypothesis.stateful.rule", "hypothesis.stateful.initialize", "hypothesis.strategies.floats", "hypothesis.strategies.lists", "hypothesis.strategies.sets", "hypothesis.strategies.frozensets", "hypothesis.strategies.iterables", "hypothesis.strategies.dictionaries", "hypothesis.strategies.characters", "hypothesis.strategies.text", "hypothesis.strategies.from_regex", "hypothesis.strategies.binary", "hypothesis.strategies.fractions", "hypothesis.strategies.decimals", "hypothesis.strategies.recursive", "hypothesis.strategies.complex_numbers", "hypothesis.strategies.shared", "hypothesis.strategies.uuids", "hypothesis.strategies.runner", "hypothesis.strategies.functions", "hypothesis.strategies.datetimes", "hypothesis.strategies.times", ) def leave_Call(self, original_node, updated_node): """Convert positional to keyword arguments.""" metadata = self.get_metadata(cst.metadata.QualifiedNameProvider, original_node) qualnames = {qn.name for qn in metadata} # If this isn't one of our known functions, or it has no posargs, stop there. if ( len(qualnames) != 1 or not qualnames.intersection(self.kwonly_functions) or not m.matches( updated_node, m.Call( func=m.DoesNotMatch(m.Call()), args=[m.Arg(keyword=None), m.ZeroOrMore()], ), ) ): return updated_node # Get the actual function object so that we can inspect the signature. # This does e.g. incur a dependency on Numpy to fix Numpy-dependent code, # but having a single source of truth about the signatures is worth it. try: params = signature(get_fn(*qualnames)).parameters.values() except ModuleNotFoundError: return updated_node # st.floats() has a new allow_subnormal kwonly argument not at the end, # so we do a bit more of a dance here. if qualnames == {"hypothesis.strategies.floats"}: params = [p for p in params if p.name != "allow_subnormal"] if len(updated_node.args) > len(params): return updated_node # Create new arg nodes with the newly required keywords assign_nospace = cst.AssignEqual( whitespace_before=cst.SimpleWhitespace(""), whitespace_after=cst.SimpleWhitespace(""), ) newargs = [ ( arg if arg.keyword or arg.star or p.kind is not Parameter.KEYWORD_ONLY else arg.with_changes(keyword=cst.Name(p.name), equal=assign_nospace) ) for p, arg in zip(params, updated_node.args, strict=False) ] return updated_node.with_changes(args=newargs) class HypothesisFixHealthCheckAll(VisitorBasedCodemodCommand): """Replace HealthCheck.all() with list(HealthCheck)""" DESCRIPTION = "Replace HealthCheck.all() with list(HealthCheck)" @m.leave(m.Call(func=m.Attribute(m.Name("HealthCheck"), m.Name("all")), args=[])) def replace_healthcheck(self, original_node, updated_node): return updated_node.with_changes( func=cst.Name("list"), args=[cst.Arg(value=cst.Name("HealthCheck"))], ) class HypothesisFixCharactersArguments(VisitorBasedCodemodCommand): """Fix deprecated white/blacklist arguments to characters:: st.characters(whitelist_categories=...) -> st.characters(categories=...) st.characters(blacklist_categories=...) -> st.characters(exclude_categories=...) st.characters(whitelist_characters=...) -> st.characters(include_characters=...) st.characters(blacklist_characters=...) -> st.characters(exclude_characters=...) Additionally, we drop `exclude_categories=` if `categories=` is present, because this argument is always redundant (or an error). """ DESCRIPTION = "Fix deprecated white/blacklist arguments to characters." METADATA_DEPENDENCIES = (cst.metadata.QualifiedNameProvider,) _replacements: ClassVar = { "whitelist_categories": "categories", "blacklist_categories": "exclude_categories", "whitelist_characters": "include_characters", "blacklist_characters": "exclude_characters", } @m.leave( m.Call( metadata=match_qualname("hypothesis.strategies.characters"), args=[ m.ZeroOrMore(), m.Arg(keyword=m.OneOf(*map(m.Name, _replacements))), m.ZeroOrMore(), ], ), ) def fn(self, original_node, updated_node): # Update to the new names newargs = [] for arg in updated_node.args: kw = self._replacements.get(arg.keyword.value, arg.keyword.value) newargs.append(arg.with_changes(keyword=cst.Name(kw))) # Drop redundant exclude_categories, which is now an error if any(m.matches(arg, m.Arg(keyword=m.Name("categories"))) for arg in newargs): ex = m.Arg(keyword=m.Name("exclude_categories")) newargs = [a for a in newargs if m.matches(a, ~ex)] return updated_node.with_changes(args=newargs) ================================================ FILE: hypothesis-python/src/hypothesis/extra/dateutil.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ This module provides :pypi:`dateutil ` timezones. You can use this strategy to make :func:`~hypothesis.strategies.datetimes` and :func:`~hypothesis.strategies.times` produce timezone-aware values. .. tip:: Consider using the stdlib :mod:`zoneinfo` module, via :func:`st.timezones() `. """ import datetime as dt from dateutil import tz, zoneinfo # type: ignore from hypothesis import strategies as st from hypothesis.strategies._internal.utils import cacheable, defines_strategy __all__ = ["timezones"] def __zone_sort_key(zone): """Sort by absolute UTC offset at reference date, positive first, with ties broken by name. """ assert zone is not None offset = zone.utcoffset(dt.datetime(2000, 1, 1)) offset = 999 if offset is None else offset return (abs(offset), -offset, str(zone)) @cacheable @defines_strategy() def timezones() -> st.SearchStrategy[dt.tzinfo]: """Any timezone from :pypi:`dateutil `. This strategy minimises to UTC, or the timezone with the smallest offset from UTC as of 2000-01-01, and is designed for use with :py:func:`~hypothesis.strategies.datetimes`. Note that the timezones generated by the strategy may vary depending on the configuration of your machine. See the dateutil documentation for more information. """ all_timezones = sorted( (tz.gettz(t) for t in zoneinfo.get_zonefile_instance().zones), key=__zone_sort_key, ) all_timezones.insert(0, tz.UTC) # We discard Nones in the list comprehension because Mypy knows that # tz.gettz may return None. However this should never happen for known # zone names, so we assert that it's impossible first. assert None not in all_timezones return st.sampled_from([z for z in all_timezones if z is not None]) ================================================ FILE: hypothesis-python/src/hypothesis/extra/django/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.extra.django._fields import from_field, register_field_strategy from hypothesis.extra.django._impl import ( LiveServerTestCase, SimpleTestCase, StaticLiveServerTestCase, TestCase, TransactionTestCase, from_form, from_model, ) __all__ = [ "LiveServerTestCase", "SimpleTestCase", "StaticLiveServerTestCase", "TestCase", "TransactionTestCase", "from_field", "from_form", "from_model", "register_field_strategy", ] ================================================ FILE: hypothesis-python/src/hypothesis/extra/django/_fields.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import string from collections.abc import Callable from datetime import datetime, timedelta from decimal import Decimal from functools import lru_cache from typing import Any, TypeAlias, TypeVar, Union import django from django import forms as df from django.conf import settings from django.core.files.base import ContentFile from django.core.validators import ( validate_ipv4_address, validate_ipv6_address, validate_ipv46_address, ) from django.db import models as dm from hypothesis import strategies as st from hypothesis.errors import InvalidArgument, ResolutionFailed from hypothesis.internal.validation import check_type from hypothesis.provisional import urls from hypothesis.strategies import emails # Use old-style union to avoid hitting # https://github.com/sphinx-doc/sphinx/issues/11211 AnyField: TypeAlias = Union[dm.Field, df.Field] # noqa: UP007 F = TypeVar("F", bound=AnyField) def numeric_bounds_from_validators( field, min_value=float("-inf"), max_value=float("inf") ): for v in field.validators: if isinstance(v, django.core.validators.MinValueValidator): min_value = max(min_value, v.limit_value) elif isinstance(v, django.core.validators.MaxValueValidator): max_value = min(max_value, v.limit_value) return min_value, max_value def integers_for_field(min_value, max_value): def inner(field): return st.integers(*numeric_bounds_from_validators(field, min_value, max_value)) return inner @lru_cache def timezones(): # From Django 4.0, the default is to use zoneinfo instead of pytz. assert getattr(django.conf.settings, "USE_TZ", False) if django.VERSION < (5, 0, 0) and getattr( django.conf.settings, "USE_DEPRECATED_PYTZ", True ): from hypothesis.extra.pytz import timezones else: from hypothesis.strategies import timezones return timezones() # Mapping of field types, to strategy objects or functions of (type) -> strategy _FieldLookUpType = dict[ type[AnyField], st.SearchStrategy | Callable[[Any], st.SearchStrategy], ] _global_field_lookup: _FieldLookUpType = { dm.SmallIntegerField: integers_for_field(-32768, 32767), dm.IntegerField: integers_for_field(-2147483648, 2147483647), dm.BigIntegerField: integers_for_field(-9223372036854775808, 9223372036854775807), dm.PositiveIntegerField: integers_for_field(0, 2147483647), dm.PositiveSmallIntegerField: integers_for_field(0, 32767), dm.BooleanField: st.booleans(), dm.DateField: st.dates(), dm.EmailField: emails(), dm.FloatField: st.floats(), dm.NullBooleanField: st.one_of(st.none(), st.booleans()), dm.URLField: urls(), dm.UUIDField: st.uuids(), df.DateField: st.dates(), df.DurationField: st.timedeltas(), df.EmailField: emails(), df.FloatField: lambda field: st.floats( *numeric_bounds_from_validators(field), allow_nan=False, allow_infinity=False ), df.IntegerField: integers_for_field(-2147483648, 2147483647), df.NullBooleanField: st.one_of(st.none(), st.booleans()), df.URLField: urls(), df.UUIDField: st.uuids(), df.FileField: st.builds( ContentFile, st.binary(min_size=1), name=st.text(min_size=1, max_size=100) ), } _ipv6_strings = st.one_of( st.ip_addresses(v=6).map(str), st.ip_addresses(v=6).map(lambda addr: addr.exploded), ) def register_for(field_type): def inner(func): _global_field_lookup[field_type] = func return func return inner @register_for(dm.DateTimeField) @register_for(df.DateTimeField) def _for_datetime(field): if getattr(django.conf.settings, "USE_TZ", False): # avoid https://code.djangoproject.com/ticket/35683 return st.datetimes( min_value=datetime.min + timedelta(days=1), max_value=datetime.max - timedelta(days=1), timezones=timezones(), ) return st.datetimes() def using_sqlite(): try: return ( getattr(django.conf.settings, "DATABASES", {}) .get("default", {}) .get("ENGINE", "") .endswith(".sqlite3") ) except django.core.exceptions.ImproperlyConfigured: return None @register_for(dm.TimeField) def _for_model_time(field): # SQLITE supports TZ-aware datetimes, but not TZ-aware times. if getattr(django.conf.settings, "USE_TZ", False) and not using_sqlite(): return st.times(timezones=timezones()) return st.times() @register_for(df.TimeField) def _for_form_time(field): if getattr(django.conf.settings, "USE_TZ", False): return st.times(timezones=timezones()) return st.times() @register_for(dm.DurationField) def _for_duration(field): # SQLite stores timedeltas as six bytes of microseconds if using_sqlite(): delta = timedelta(microseconds=2**47 - 1) return st.timedeltas(-delta, delta) return st.timedeltas() @register_for(dm.SlugField) @register_for(df.SlugField) def _for_slug(field): min_size = 1 if getattr(field, "blank", False) or not getattr(field, "required", True): min_size = 0 return st.text( alphabet=string.ascii_letters + string.digits, min_size=min_size, max_size=field.max_length, ) @register_for(dm.GenericIPAddressField) def _for_model_ip(field): return { "ipv4": st.ip_addresses(v=4).map(str), "ipv6": _ipv6_strings, "both": st.ip_addresses(v=4).map(str) | _ipv6_strings, }[field.protocol.lower()] @register_for(df.GenericIPAddressField) def _for_form_ip(field): # the IP address form fields have no direct indication of which type # of address they want, so direct comparison with the validator # function has to be used instead. Sorry for the potato logic here if validate_ipv46_address in field.default_validators: return st.ip_addresses(v=4).map(str) | _ipv6_strings if validate_ipv4_address in field.default_validators: return st.ip_addresses(v=4).map(str) if validate_ipv6_address in field.default_validators: return _ipv6_strings raise ResolutionFailed(f"No IP version validator on {field=}") @register_for(dm.DecimalField) @register_for(df.DecimalField) def _for_decimal(field): min_value, max_value = numeric_bounds_from_validators(field) bound = Decimal(10**field.max_digits - 1) / (10**field.decimal_places) return st.decimals( min_value=max(min_value, -bound), max_value=min(max_value, bound), places=field.decimal_places, ) def length_bounds_from_validators(field): min_size = 1 max_size = field.max_length for v in field.validators: if isinstance(v, django.core.validators.MinLengthValidator): min_size = max(min_size, v.limit_value) elif isinstance(v, django.core.validators.MaxLengthValidator): max_size = min(max_size or v.limit_value, v.limit_value) return min_size, max_size @register_for(dm.BinaryField) def _for_binary(field): min_size, max_size = length_bounds_from_validators(field) if getattr(field, "blank", False) or not getattr(field, "required", True): return st.just(b"") | st.binary(min_size=min_size, max_size=max_size) return st.binary(min_size=min_size, max_size=max_size) @register_for(dm.CharField) @register_for(dm.TextField) @register_for(df.CharField) @register_for(df.RegexField) def _for_text(field): # We can infer a vastly more precise strategy by considering the # validators as well as the field type. This is a minimal proof of # concept, but we intend to leverage the idea much more heavily soon. # See https://github.com/HypothesisWorks/hypothesis-python/issues/1116 regexes = [ re.compile(v.regex, v.flags) if isinstance(v.regex, str) else v.regex for v in field.validators if isinstance(v, django.core.validators.RegexValidator) and not v.inverse_match ] if regexes: # This strategy generates according to one of the regexes, and # filters using the others. It can therefore learn to generate # from the most restrictive and filter with permissive patterns. # Not maximally efficient, but it makes pathological cases rarer. # If you want a challenge: extend https://qntm.org/greenery to # compute intersections of the full Python regex language. return st.one_of(*(st.from_regex(r) for r in regexes)) # If there are no (usable) regexes, we use a standard text strategy. min_size, max_size = length_bounds_from_validators(field) strategy = st.text( alphabet=st.characters(exclude_characters="\x00", exclude_categories=("Cs",)), min_size=min_size, max_size=max_size, ).filter(lambda s: min_size <= len(s.strip())) if getattr(field, "blank", False) or not getattr(field, "required", True): return st.just("") | strategy return strategy if "django.contrib.auth" in settings.INSTALLED_APPS: from django.contrib.auth.forms import UsernameField register_for(UsernameField)(_for_text) @register_for(df.BooleanField) def _for_form_boolean(field): if field.required: return st.just(True) return st.booleans() def _model_choice_strategy(field): def _strategy(): if field.choices is None: # The field was instantiated with queryset=None, and not # subsequently updated. raise InvalidArgument( "Cannot create strategy for ModelChoicesField with no choices" ) elif hasattr(field, "_choices"): # The choices property was set manually. choices = field._choices else: # choices is not None, and was not set manually, so we # must have a QuerySet. choices = field.queryset if not choices.ordered: raise InvalidArgument( f"Cannot create strategy for {field.__class__.__name__} with a choices " "attribute derived from a QuerySet without an explicit ordering - this may " "cause Hypothesis to produce unstable results between runs." ) return st.sampled_from( [ ( choice.value if isinstance(choice, df.models.ModelChoiceIteratorValue) else choice # Empty value, if included. ) for choice, _ in field.choices ] ) # Accessing field.choices causes database access, so defer the strategy. return st.deferred(_strategy) @register_for(df.ModelChoiceField) def _for_model_choice(field): return _model_choice_strategy(field) @register_for(df.ModelMultipleChoiceField) def _for_model_multiple_choice(field): min_size = 1 if field.required else 0 return st.lists(_model_choice_strategy(field), min_size=min_size, unique=True) def register_field_strategy( field_type: type[AnyField], strategy: st.SearchStrategy ) -> None: """Add an entry to the global field-to-strategy lookup used by :func:`~hypothesis.extra.django.from_field`. ``field_type`` must be a subtype of :class:`django.db.models.Field` or :class:`django.forms.Field`, which must not already be registered. ``strategy`` must be a :class:`~hypothesis.strategies.SearchStrategy`. """ if not issubclass(field_type, (dm.Field, df.Field)): raise InvalidArgument(f"{field_type=} must be a subtype of Field") check_type(st.SearchStrategy, strategy, "strategy") if field_type in _global_field_lookup: raise InvalidArgument( f"{field_type=} already has a registered " f"strategy ({_global_field_lookup[field_type]!r})" ) if issubclass(field_type, dm.AutoField): raise InvalidArgument("Cannot register a strategy for an AutoField") _global_field_lookup[field_type] = strategy def from_field(field: F) -> st.SearchStrategy[F | None]: """Return a strategy for values that fit the given field. This function is used by :func:`~hypothesis.extra.django.from_form` and :func:`~hypothesis.extra.django.from_model` for any fields that require a value, or for which you passed ``...`` (:obj:`python:Ellipsis`) to infer a strategy from an annotation. It's pretty similar to the core :func:`~hypothesis.strategies.from_type` function, with a subtle but important difference: ``from_field`` takes a Field *instance*, rather than a Field *subtype*, so that it has access to instance attributes such as string length and validators. """ check_type((dm.Field, df.Field), field, "field") # The following isinstance check must occur *before* the getattr # check. In the case of ModelChoicesField, evaluating # field.choices causes database access, which we want to avoid if # we don't have a connection (the generated strategies for # ModelChoicesField defer evaluation of `choices'). if not isinstance(field, df.ModelChoiceField) and getattr(field, "choices", False): choices: list = [] for value, name_or_optgroup in field.choices: if isinstance(name_or_optgroup, (list, tuple)): choices.extend(key for key, _ in name_or_optgroup) else: choices.append(value) # form fields automatically include an empty choice, strip it out if "" in choices: choices.remove("") min_size = 1 if isinstance(field, (dm.CharField, dm.TextField)) and field.blank: choices.insert(0, "") elif isinstance(field, (df.Field)) and not field.required: choices.insert(0, "") min_size = 0 strategy = st.sampled_from(choices) if isinstance(field, (df.MultipleChoiceField, df.TypedMultipleChoiceField)): strategy = st.lists(st.sampled_from(choices), min_size=min_size) else: if type(field) not in _global_field_lookup: if getattr(field, "null", False): return st.none() raise ResolutionFailed(f"Could not infer a strategy for {field!r}") strategy = _global_field_lookup[type(field)] # type: ignore if not isinstance(strategy, st.SearchStrategy): strategy = strategy(field) assert isinstance(strategy, st.SearchStrategy) if field.validators: def validate(value): try: field.run_validators(value) return True except django.core.exceptions.ValidationError: return False strategy = strategy.filter(validate) if getattr(field, "null", False): return st.none() | strategy return strategy ================================================ FILE: hypothesis-python/src/hypothesis/extra/django/_impl.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import unittest from functools import partial from types import EllipsisType from typing import Any, TypeVar from django import forms as df, test as dt from django.contrib.staticfiles import testing as dst from django.core.exceptions import ValidationError from django.db import IntegrityError, models as dm from hypothesis import reject, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra.django._fields import from_field from hypothesis.strategies._internal.utils import defines_strategy ModelT = TypeVar("ModelT", bound=dm.Model) class HypothesisTestCase: def setup_example(self): self._pre_setup() def teardown_example(self, example): self._post_teardown() def __call__(self, result=None): testMethod = getattr(self, self._testMethodName) if getattr(testMethod, "is_hypothesis_test", False): return unittest.TestCase.__call__(self, result) else: return dt.SimpleTestCase.__call__(self, result) class SimpleTestCase(HypothesisTestCase, dt.SimpleTestCase): pass class TestCase(HypothesisTestCase, dt.TestCase): pass class TransactionTestCase(HypothesisTestCase, dt.TransactionTestCase): pass class LiveServerTestCase(HypothesisTestCase, dt.LiveServerTestCase): pass class StaticLiveServerTestCase(HypothesisTestCase, dst.StaticLiveServerTestCase): pass @defines_strategy() def from_model( model: type[ModelT], /, **field_strategies: st.SearchStrategy | EllipsisType ) -> st.SearchStrategy[ModelT]: """Return a strategy for examples of ``model``. .. warning:: Hypothesis creates saved models. This will run inside your testing transaction when using the test runner, but if you use the dev console this will leave debris in your database. ``model`` must be an subclass of :class:`~django:django.db.models.Model`. Strategies for fields may be passed as keyword arguments, for example ``is_staff=st.just(False)``. In order to support models with fields named "model", this is a positional-only parameter. Hypothesis can often infer a strategy based the field type and validators, and will attempt to do so for any required fields. No strategy will be inferred for an :class:`~django:django.db.models.AutoField`, nullable field, foreign key, or field for which a keyword argument is passed to ``from_model()``. For example, a Shop type with a foreign key to Company could be generated with:: shop_strategy = from_model(Shop, company=from_model(Company)) Like for :func:`~hypothesis.strategies.builds`, you can pass ``...`` (:obj:`python:Ellipsis`) as a keyword argument to infer a strategy for a field which has a default value instead of using the default. """ if not issubclass(model, dm.Model): raise InvalidArgument(f"{model=} must be a subtype of Model") fields_by_name = {f.name: f for f in model._meta.concrete_fields} for name, value in sorted(field_strategies.items()): if value is ...: field_strategies[name] = from_field(fields_by_name[name]) for name, field in sorted(fields_by_name.items()): if ( name not in field_strategies and not field.auto_created and not isinstance(field, dm.AutoField) and not isinstance(field, getattr(dm, "GeneratedField", ())) and field.default is dm.fields.NOT_PROVIDED ): field_strategies[name] = from_field(field) for field in field_strategies: if model._meta.get_field(field).primary_key: # The primary key is generated as part of the strategy. We # want to find any existing row with this primary key and # overwrite its contents. kwargs = {field: field_strategies.pop(field)} kwargs["defaults"] = st.fixed_dictionaries(field_strategies) # type: ignore return _models_impl(st.builds(model.objects.update_or_create, **kwargs)) # The primary key is not generated as part of the strategy, so we # just match against any row that has the same value for all # fields. return _models_impl(st.builds(model.objects.get_or_create, **field_strategies)) @st.composite def _models_impl(draw, strat): """Handle the nasty part of drawing a value for models()""" try: return draw(strat)[0] except IntegrityError: reject() @defines_strategy() def from_form( form: type[df.Form], form_kwargs: dict | None = None, **field_strategies: st.SearchStrategy | EllipsisType, ) -> st.SearchStrategy[df.Form]: """Return a strategy for examples of ``form``. ``form`` must be an subclass of :class:`~django:django.forms.Form`. Strategies for fields may be passed as keyword arguments, for example ``is_staff=st.just(False)``. Hypothesis can often infer a strategy based the field type and validators, and will attempt to do so for any required fields. No strategy will be inferred for a disabled field or field for which a keyword argument is passed to ``from_form()``. This function uses the fields of an unbound ``form`` instance to determine field strategies, any keyword arguments needed to instantiate the unbound ``form`` instance can be passed into ``from_form()`` as a dict with the keyword ``form_kwargs``. E.g.:: shop_strategy = from_form(Shop, form_kwargs={"company_id": 5}) Like for :func:`~hypothesis.strategies.builds`, you can pass ``...`` (:obj:`python:Ellipsis`) as a keyword argument to infer a strategy for a field which has a default value instead of using the default. """ # currently unsupported: # ComboField # FilePathField # ImageField form_kwargs = form_kwargs or {} if not issubclass(form, df.BaseForm): raise InvalidArgument(f"{form=} must be a subtype of Form") # Forms are a little bit different from models. Model classes have # all their fields defined, whereas forms may have different fields # per-instance. So, we ought to instantiate the form and get the # fields from the instance, thus we need to accept the kwargs for # instantiation as well as the explicitly defined strategies unbound_form = form(**form_kwargs) fields_by_name = {} for name, field in unbound_form.fields.items(): if isinstance(field, df.MultiValueField): # PS: So this is a little strange, but MultiValueFields must # have their form data encoded in a particular way for the # values to actually be picked up by the widget instances' # ``value_from_datadict``. # E.g. if a MultiValueField named 'mv_field' has 3 # sub-fields then the ``value_from_datadict`` will look for # 'mv_field_0', 'mv_field_1', and 'mv_field_2'. Here I'm # decomposing the individual sub-fields into the names that # the form validation process expects for i, _field in enumerate(field.fields): fields_by_name[f"{name}_{i}"] = _field else: fields_by_name[name] = field for name, value in sorted(field_strategies.items()): if value is ...: field_strategies[name] = from_field(fields_by_name[name]) for name, field in sorted(fields_by_name.items()): if name not in field_strategies and not field.disabled: field_strategies[name] = from_field(field) # files are handled a bit specially in forms. A Form accepts two arguments: # `data` and `files`. The former is for normal fields, and the latter is for # file fields. # see https://docs.djangoproject.com/en/5.1/ref/forms/api/#binding-uploaded-files. data_strategies: dict[str, Any] = {} file_strategies: dict[str, Any] = {} for name, field in field_strategies.items(): form_field = fields_by_name[name] dictionary = ( file_strategies if isinstance(form_field, df.FileField) else data_strategies ) dictionary[name] = field return _forms_impl( st.builds( partial(form, **form_kwargs), # type: ignore data=st.fixed_dictionaries(data_strategies), files=st.fixed_dictionaries(file_strategies), ) ) @st.composite def _forms_impl(draw, strat): """Handle the nasty part of drawing a value for from_form()""" try: return draw(strat) except ValidationError: reject() ================================================ FILE: hypothesis-python/src/hypothesis/extra/dpcontracts.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ This module provides tools for working with the :pypi:`dpcontracts` library, because `combining contracts and property-based testing works really well `_. It requires ``dpcontracts >= 0.4``. """ from dpcontracts import PreconditionError from hypothesis import reject from hypothesis.errors import InvalidArgument from hypothesis.internal.reflection import proxies def fulfill(contract_func): """Decorate ``contract_func`` to reject calls which violate preconditions, and retry them with different arguments. This is a convenience function for testing internal code that uses :pypi:`dpcontracts`, to automatically filter out arguments that would be rejected by the public interface before triggering a contract error. This can be used as ``builds(fulfill(func), ...)`` or in the body of the test e.g. ``assert fulfill(func)(*args)``. """ if not hasattr(contract_func, "__contract_wrapped_func__"): raise InvalidArgument( f"{contract_func.__name__} has no dpcontracts preconditions" ) @proxies(contract_func) def inner(*args, **kwargs): try: return contract_func(*args, **kwargs) except PreconditionError: reject() return inner ================================================ FILE: hypothesis-python/src/hypothesis/extra/ghostwriter.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ Writing tests with Hypothesis frees you from the tedium of deciding on and writing out specific inputs to test. Now, the ``hypothesis.extra.ghostwriter`` module can write your test functions for you too! The idea is to provide **an easy way to start** property-based testing, **and a seamless transition** to more complex test code - because ghostwritten tests are source code that you could have written for yourself. So just pick a function you'd like tested, and feed it to one of the functions below. They follow imports, use but do not require type annotations, and generally do their best to write you a useful test. You can also use :ref:`our command-line interface `:: $ hypothesis write --help Usage: hypothesis write [OPTIONS] FUNC... `hypothesis write` writes property-based tests for you! Type annotations are helpful but not required for our advanced introspection and templating logic. Try running the examples below to see how it works: hypothesis write gzip hypothesis write numpy.matmul hypothesis write pandas.from_dummies hypothesis write re.compile --except re.error hypothesis write --equivalent ast.literal_eval eval hypothesis write --roundtrip json.dumps json.loads hypothesis write --style=unittest --idempotent sorted hypothesis write --binary-op operator.add Options: --roundtrip start by testing write/read or encode/decode! --equivalent very useful when optimising or refactoring code --errors-equivalent --equivalent, but also allows consistent errors --idempotent check that f(x) == f(f(x)) --binary-op associativity, commutativity, identity element --style [pytest|unittest] pytest-style function, or unittest-style method? -e, --except OBJ_NAME dotted name of exception(s) to ignore --annotate / --no-annotate force ghostwritten tests to be type-annotated (or not). By default, match the code to test. -h, --help Show this message and exit. .. tip:: Using a light theme? Hypothesis respects `NO_COLOR `__ and ``DJANGO_COLORS=light``. .. note:: The ghostwriter requires :pypi:`black`, but the generated code only requires Hypothesis itself. .. note:: Legal questions? While the ghostwriter fragments and logic is under the MPL-2.0 license like the rest of Hypothesis, the *output* from the ghostwriter is made available under the `Creative Commons Zero (CC0) `__ public domain dedication, so you can use it without any restrictions. """ import ast import builtins import contextlib import enum import inspect import os import re import sys import types import warnings from collections import OrderedDict, defaultdict from collections.abc import Callable, Iterable, Mapping from itertools import permutations, zip_longest from keyword import iskeyword as _iskeyword from string import ascii_lowercase from textwrap import dedent, indent from types import EllipsisType from typing import ( Any, ForwardRef, NamedTuple, TypeVar, get_args, get_origin, ) import black from hypothesis import Verbosity, find, settings, strategies as st from hypothesis.errors import InvalidArgument, SmallSearchSpaceWarning from hypothesis.internal.compat import get_type_hints from hypothesis.internal.reflection import get_signature, is_mock from hypothesis.internal.validation import check_type from hypothesis.provisional import domains from hypothesis.strategies._internal.collections import ListStrategy from hypothesis.strategies._internal.core import BuildsStrategy from hypothesis.strategies._internal.deferred import DeferredStrategy from hypothesis.strategies._internal.flatmapped import FlatMapStrategy from hypothesis.strategies._internal.lazy import LazyStrategy, unwrap_strategies from hypothesis.strategies._internal.strategies import ( FilteredStrategy, MappedStrategy, OneOfStrategy, SampledFromStrategy, ) from hypothesis.strategies._internal.types import _global_type_lookup, is_generic_type IMPORT_SECTION = """ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. {imports} """ TEMPLATE = """ @given({given_args}) def test_{test_kind}_{func_name}({arg_names}){return_annotation}: {test_body} """ SUPPRESS_BLOCK = """ try: {test_body} except {exceptions}: reject() """.strip() Except = type[Exception] | tuple[type[Exception], ...] ImportSet = set[str | tuple[str, str]] _quietly_settings = settings( database=None, deadline=None, derandomize=True, verbosity=Verbosity.quiet, ) def _dedupe_exceptions(exc: tuple[type[Exception], ...]) -> tuple[type[Exception], ...]: # This is reminiscent of de-duplication logic I wrote for flake8-bugbear, # but with access to the actual objects we can just check for subclasses. # This lets us print e.g. `Exception` instead of `(Exception, OSError)`. uniques = list(exc) for a, b in permutations(exc, 2): if a in uniques and issubclass(a, b): uniques.remove(a) return tuple(sorted(uniques, key=lambda e: e.__name__)) def _check_except(except_: Except) -> tuple[type[Exception], ...]: if isinstance(except_, tuple): for i, e in enumerate(except_): if not isinstance(e, type) or not issubclass(e, Exception): raise InvalidArgument( f"Expected an Exception but got except_[{i}]={e!r}" f" (type={_get_qualname(type(e))})" ) return except_ if not isinstance(except_, type) or not issubclass(except_, Exception): raise InvalidArgument( "Expected an Exception or tuple of exceptions, but got except_=" f"{except_!r} (type={_get_qualname(type(except_))})" ) return (except_,) def _exception_string(except_: tuple[type[Exception], ...]) -> tuple[ImportSet, str]: if not except_: return set(), "" exceptions = [] imports: ImportSet = set() for ex in _dedupe_exceptions(except_): if ex.__qualname__ in dir(builtins): exceptions.append(ex.__qualname__) else: imports.add(ex.__module__) exceptions.append(_get_qualname(ex, include_module=True)) return imports, ( "(" + ", ".join(exceptions) + ")" if len(exceptions) > 1 else exceptions[0] ) def _check_style(style: str) -> None: if style not in ("pytest", "unittest"): raise InvalidArgument(f"Valid styles are 'pytest' or 'unittest', got {style!r}") def _exceptions_from_docstring(doc: str) -> tuple[type[Exception], ...]: """Return a tuple of exceptions that the docstring says may be raised. Note that we ignore non-builtin exception types for simplicity, as this is used directly in _write_call() and passing import sets around would be really really annoying. """ # TODO: it would be great to handle Google- and Numpy-style docstrings # (e.g. by using the Napoleon Sphinx extension) assert isinstance(doc, str), doc raises = [] for excname in re.compile(r"\:raises\s+(\w+)\:", re.MULTILINE).findall(doc): exc_type = getattr(builtins, excname, None) if isinstance(exc_type, type) and issubclass(exc_type, Exception): raises.append(exc_type) return tuple(_dedupe_exceptions(tuple(raises))) def _type_from_doc_fragment(token: str) -> type | None: # Special cases for "integer" and for numpy array-like and dtype if token == "integer": return int if "numpy" in sys.modules: if re.fullmatch(r"[Aa]rray[-_ ]?like", token): return sys.modules["numpy"].ndarray elif token == "dtype": return sys.modules["numpy"].dtype # Natural-language syntax, e.g. "sequence of integers" coll_match = re.fullmatch(r"(\w+) of (\w+)", token) if coll_match is not None: coll_token, elem_token = coll_match.groups() elems = _type_from_doc_fragment(elem_token) if elems is None and elem_token.endswith("s"): elems = _type_from_doc_fragment(elem_token[:-1]) if elems is not None and coll_token in ("list", "sequence", "collection"): return list[elems] # type: ignore # This might be e.g. "array-like of float"; arrays is better than nothing # even if we can't conveniently pass a generic type around. return _type_from_doc_fragment(coll_token) # Check either builtins, or the module for a dotted name if "." not in token: return getattr(builtins, token, None) mod, name = token.rsplit(".", maxsplit=1) return getattr(sys.modules.get(mod, None), name, None) def _strip_typevars(type_): with contextlib.suppress(Exception): if {type(a) for a in get_args(type_)} == {TypeVar}: return get_origin(type_) return type_ def _strategy_for(param: inspect.Parameter, docstring: str) -> st.SearchStrategy: # Example types in docstrings: # - `:type a: sequence of integers` # - `b (list, tuple, or None): ...` # - `c : {"foo", "bar", or None}` for pattern in ( rf"^\s*\:type\s+{param.name}\:\s+(.+)", # RST-style rf"^\s*{param.name} \((.+)\):", # Google-style rf"^\s*{param.name} \: (.+)", # Numpy-style ): match = re.search(pattern, docstring, flags=re.MULTILINE) if match is None: continue doc_type = match.group(1) doc_type = doc_type.removesuffix(", optional").strip("}{") elements = [] types = [] for token in re.split(r",? +or +| *, *", doc_type): for prefix in ("default ", "python "): # e.g. `str or None, default "auto"` or `python int or numpy.int64` token = token.removeprefix(prefix) if not token: continue try: # Elements of `{"inner", "outer"}` etc. elements.append(ast.literal_eval(token)) continue except (ValueError, SyntaxError): t = _type_from_doc_fragment(token) if isinstance(t, type) or is_generic_type(t): assert t is not None types.append(_strip_typevars(t)) if ( param.default is not inspect.Parameter.empty and param.default not in elements and not isinstance( param.default, tuple(t for t in types if isinstance(t, type)) ) ): with contextlib.suppress(SyntaxError): compile(repr(st.just(param.default)), "", "eval") elements.insert(0, param.default) if elements or types: return (st.sampled_from(elements) if elements else st.nothing()) | ( st.one_of(*map(st.from_type, types)) if types else st.nothing() ) # If our default value is an Enum or a boolean, we assume that any value # of that type is acceptable. Otherwise, we only generate the default. if isinstance(param.default, bool): return st.booleans() if isinstance(param.default, enum.Enum): return st.sampled_from(type(param.default)) if param.default is not inspect.Parameter.empty: # Using `st.from_type(type(param.default))` would introduce spurious # failures in cases like the `flags` argument to regex functions. # Better in to keep it simple, and let the user elaborate if desired. return st.just(param.default) return _guess_strategy_by_argname(name=param.name.lower()) # fmt: off BOOL_NAMES = ( "keepdims", "verbose", "debug", "force", "train", "training", "trainable", "bias", "shuffle", "show", "load", "pretrained", "save", "overwrite", "normalize", "reverse", "success", "enabled", "strict", "copy", "quiet", "required", "inplace", "recursive", "enable", "active", "create", "validate", "refresh", "use_bias", ) POSITIVE_INTEGER_NAMES = ( "width", "size", "length", "limit", "idx", "stride", "epoch", "epochs", "depth", "pid", "steps", "iteration", "iterations", "vocab_size", "ttl", "count", ) FLOAT_NAMES = ( "real", "imag", "alpha", "theta", "beta", "sigma", "gamma", "angle", "reward", "tau", "temperature", ) STRING_NAMES = ( "text", "txt", "password", "label", "prefix", "suffix", "desc", "description", "str", "pattern", "subject", "reason", "comment", "prompt", "sentence", "sep", ) # fmt: on def _guess_strategy_by_argname(name: str) -> st.SearchStrategy: """ If all else fails, we try guessing a strategy based on common argument names. We wouldn't do this in builds() where strict correctness is required, but for the ghostwriter we accept "good guesses" since the user would otherwise have to change the strategy anyway - from `nothing()` - if we refused to guess. A "good guess" is _usually correct_, and _a reasonable mistake_ if not. The logic below is therefore based on a manual reading of the builtins and some standard-library docs, plus the analysis of about three hundred million arguments in https://github.com/HypothesisWorks/hypothesis/issues/3311 """ # Special-cased names if name in ("function", "func", "f"): return st.functions() if name in ("pred", "predicate"): return st.functions(returns=st.booleans(), pure=True) if name in ("iterable",): return st.iterables(st.integers()) | st.iterables(st.text()) if name in ("list", "lst", "ls"): return st.lists(st.nothing()) if name in ("object",): return st.builds(object) if "uuid" in name: return st.uuids().map(str) # Names which imply the value is a boolean if name.startswith("is_") or name in BOOL_NAMES: return st.booleans() # Names which imply that the value is a number, perhaps in a particular range if name in ("amount", "threshold", "number", "num"): return st.integers() | st.floats() if name in ("port",): return st.integers(0, 2**16 - 1) if ( name.endswith("_size") or (name.endswith("size") and "_" not in name) or re.fullmatch(r"n(um)?_[a-z_]*s", name) or name in POSITIVE_INTEGER_NAMES ): return st.integers(min_value=0) if name in ("offset", "seed", "dim", "total", "priority"): return st.integers() if name in ("learning_rate", "dropout", "dropout_rate", "epsilon", "eps", "prob"): return st.floats(0, 1) if name in ("lat", "latitude"): return st.floats(-90, 90) if name in ("lon", "longitude"): return st.floats(-180, 180) if name in ("radius", "tol", "tolerance", "rate"): return st.floats(min_value=0) if name in FLOAT_NAMES: return st.floats() # Names which imply that the value is a string if name in ("host", "hostname"): return domains() if name in ("email",): return st.emails() if name in ("word", "slug", "api_key"): return st.from_regex(r"\w+", fullmatch=True) if name in ("char", "character"): return st.characters() if ( "file" in name or "path" in name or name.endswith("_dir") or name in ("fname", "dir", "dirname", "directory", "folder") ): # Common names for filesystem paths: these are usually strings, but we # don't want to make strings more convenient than pathlib.Path. return st.nothing() if ( name.endswith(("_name", "label")) or (name.endswith("name") and "_" not in name) or ("string" in name and "as" not in name) or name in STRING_NAMES ): return st.text() # Last clever idea: maybe we're looking a plural, and know the singular: if re.fullmatch(r"\w*[^s]s", name): elems = _guess_strategy_by_argname(name[:-1]) if not elems.is_empty: return st.lists(elems) # And if all that failed, we'll return nothing() - the user will have to # fill this in by hand, and we'll leave a comment to that effect later. return st.nothing() def _get_params_builtin_fn(func: Callable) -> list[inspect.Parameter]: if ( isinstance(func, (types.BuiltinFunctionType, types.BuiltinMethodType)) and hasattr(func, "__doc__") and isinstance(func.__doc__, str) ): # inspect.signature doesn't work on all builtin functions or methods. # In such cases, we can try to reconstruct simple signatures from the docstring. match = re.match(rf"^{func.__name__}\((.+?)\)", func.__doc__) if match is None: return [] args = match.group(1).replace("[", "").replace("]", "") params = [] # Even if the signature doesn't contain a /, we assume that arguments # are positional-only until shown otherwise - the / is often omitted. kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_ONLY for arg in args.split(", "): arg, *_ = arg.partition("=") arg = arg.strip() if arg == "/": kind = inspect.Parameter.POSITIONAL_OR_KEYWORD continue if arg.startswith("*") or arg == "...": kind = inspect.Parameter.KEYWORD_ONLY continue # we omit *varargs, if there are any if _iskeyword(arg.lstrip("*")) or not arg.lstrip("*").isidentifier(): break # skip all subsequent params if this name is invalid params.append(inspect.Parameter(name=arg, kind=kind)) return params return [] def _get_params_ufunc(func: Callable) -> list[inspect.Parameter]: if _is_probably_ufunc(func): # `inspect.signature` results vary for ufunc objects, but we can work out # what the required parameters would look like if it was reliable. # Note that we use args named a, b, c... to match the `operator` module, # rather than x1, x2, x3... like the Numpy docs. Because they're pos-only # this doesn't make a runtime difference, and it's much nicer for use-cases # like `equivalent(numpy.add, operator.add)`. return [ inspect.Parameter(name=name, kind=inspect.Parameter.POSITIONAL_ONLY) for name in ascii_lowercase[: func.nin] # type: ignore ] return [] def _get_params(func: Callable) -> dict[str, inspect.Parameter]: """Get non-vararg parameters of `func` as an ordered dict.""" try: params = list(get_signature(func).parameters.values()) except Exception: if params := _get_params_ufunc(func): pass elif params := _get_params_builtin_fn(func): pass else: # If we haven't managed to recover a signature through the tricks above, # we're out of ideas and should just re-raise the exception. raise else: P = inspect.Parameter placeholder = [("args", P.VAR_POSITIONAL), ("kwargs", P.VAR_KEYWORD)] if ufunc_params := _get_params_ufunc(func): # If func is a ufunc, prefer _get_params_ufunc over get_signature, # as the latter includes keyword arguments we aren't well-equipped # to ghostwrite. params = ufunc_params elif [(p.name, p.kind) for p in params] == placeholder: # If the params we got look like an uninformative placeholder, try fallbacks. params = _get_params_builtin_fn(func) or params return _params_to_dict(params) def _params_to_dict( params: Iterable[inspect.Parameter], ) -> dict[str, inspect.Parameter]: var_param_kinds = (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) return OrderedDict((p.name, p) for p in params if p.kind not in var_param_kinds) @contextlib.contextmanager def _with_any_registered(): # If the user has registered their own strategy for Any, leave it alone if Any in _global_type_lookup: yield # We usually want to force from_type(Any) to raise an error because we don't # have enough information to accurately resolve user intent, but in this case # we can treat it as a synonym for object - this is probably wrong, but you'll # get at least _some_ output to edit later. We then reset everything in order # to avoid polluting the resolution logic in case you run tests later. else: try: _global_type_lookup[Any] = st.builds(object) yield finally: del _global_type_lookup[Any] st.from_type.__clear_cache() def _get_strategies( *funcs: Callable, pass_result_to_next_func: bool = False ) -> dict[str, st.SearchStrategy]: """Return a dict of strategies for the union of arguments to `funcs`. If `pass_result_to_next_func` is True, assume that the result of each function is passed to the next, and therefore skip the first argument of all but the first function. This dict is used to construct our call to the `@given(...)` decorator. """ assert funcs, "Must pass at least one function" given_strategies: dict[str, st.SearchStrategy] = {} for i, f in enumerate(funcs): params = _get_params(f) if pass_result_to_next_func and i >= 1: del params[next(iter(params))] hints = get_type_hints(f) docstring = getattr(f, "__doc__", None) or "" builder_args = { k: ... if k in hints else _strategy_for(v, docstring) for k, v in params.items() } with _with_any_registered(): strat = st.builds(f, **builder_args).wrapped_strategy # type: ignore if strat.args: raise NotImplementedError("Expected to pass everything as kwargs") for k, v in strat.kwargs.items(): if _valid_syntax_repr(v)[1] == "nothing()" and k in hints: # e.g. from_type(Hashable) is OK but the unwrapped repr is not v = LazyStrategy(st.from_type, (hints[k],), {}) if k in given_strategies: given_strategies[k] |= v else: given_strategies[k] = v # If there is only one function, we pass arguments to @given in the order of # that function's signature. Otherwise, we use alphabetical order. if len(funcs) == 1: return {name: given_strategies[name] for name in _get_params(f)} return dict(sorted(given_strategies.items())) def _assert_eq(style: str, a: str, b: str) -> str: if style == "unittest": return f"self.assertEqual({a}, {b})" assert style == "pytest" if a.isidentifier() and b.isidentifier(): return f"assert {a} == {b}, ({a}, {b})" return f"assert {a} == {b}" def _imports_for_object(obj): """Return the imports for `obj`, which may be empty for e.g. lambdas""" if type(obj) is getattr(types, "UnionType", object()): return {mod for mod, _ in set().union(*map(_imports_for_object, obj.__args__))} if isinstance(obj, (re.Pattern, re.Match)): return {"re"} if isinstance(obj, st.SearchStrategy): return _imports_for_strategy(obj) if isinstance(obj, getattr(sys.modules.get("numpy"), "dtype", ())): return {("numpy", "dtype")} try: if is_generic_type(obj): if isinstance(obj, TypeVar): return {(obj.__module__, obj.__name__)} with contextlib.suppress(Exception): return set().union(*map(_imports_for_object, obj.__args__)) if (not callable(obj)) or obj.__name__ == "": return set() name = _get_qualname(obj).split(".")[0] return {(_get_module(obj), name)} except Exception: return set() def _imports_for_strategy(strategy): # If we have a lazy from_type strategy, because unwrapping it gives us an # error or invalid syntax, import that type and we're done. if isinstance(strategy, LazyStrategy): imports = { imp for arg in set(strategy._LazyStrategy__args) | set(strategy._LazyStrategy__kwargs.values()) for imp in _imports_for_object(_strip_typevars(arg)) } if re.match(r"from_(type|regex)\(", repr(strategy)): return imports elif _get_module(strategy.function).startswith("hypothesis.extra."): module = _get_module(strategy.function).replace("._array_helpers", ".numpy") return {(module, strategy.function.__name__)} | imports imports = set() with warnings.catch_warnings(): warnings.simplefilter("ignore", SmallSearchSpaceWarning) strategy = unwrap_strategies(strategy) # Get imports for s.map(f), s.filter(f), s.flatmap(f), including both s and f if isinstance(strategy, MappedStrategy): imports |= _imports_for_strategy(strategy.mapped_strategy) imports |= _imports_for_object(strategy.pack) if isinstance(strategy, FilteredStrategy): imports |= _imports_for_strategy(strategy.filtered_strategy) for f in strategy.flat_conditions: imports |= _imports_for_object(f) if isinstance(strategy, FlatMapStrategy): imports |= _imports_for_strategy(strategy.base) imports |= _imports_for_object(strategy.expand) # recurse through one_of to handle e.g. from_type(Optional[Foo]) if isinstance(strategy, OneOfStrategy): for s in strategy.element_strategies: imports |= _imports_for_strategy(s) # get imports for the target of builds(), and recurse into the argument strategies if isinstance(strategy, BuildsStrategy): imports |= _imports_for_object(strategy.target) for s in strategy.args: imports |= _imports_for_strategy(s) for s in strategy.kwargs.values(): imports |= _imports_for_strategy(s) if isinstance(strategy, SampledFromStrategy): for obj in strategy.elements: imports |= _imports_for_object(obj) if isinstance(strategy, ListStrategy): imports |= _imports_for_strategy(strategy.element_strategy) return imports def _valid_syntax_repr(strategy): # For binary_op, we pass a variable name - so pass it right back again. if isinstance(strategy, str): return set(), strategy # Flatten and de-duplicate any one_of strategies, whether that's from resolving # a Union type or combining inputs to multiple functions. try: if isinstance(strategy, DeferredStrategy): strategy = strategy.wrapped_strategy if isinstance(strategy, OneOfStrategy): seen = set() elems = [] with warnings.catch_warnings(): warnings.simplefilter("ignore", SmallSearchSpaceWarning) strategy.element_strategies # might warn on first access for s in strategy.element_strategies: if isinstance(s, SampledFromStrategy) and s.elements == (os.environ,): continue if repr(s) not in seen: elems.append(s) seen.add(repr(s)) strategy = st.one_of(elems or st.nothing()) # hardcode some special cases for nicer reprs if strategy == st.text().wrapped_strategy: return set(), "text()" if strategy == st.from_type(type): return set(), "from_type(type)" # Remove any typevars; we don't exploit them so they're just clutter here if ( isinstance(strategy, LazyStrategy) and strategy.function.__name__ == st.from_type.__name__ and strategy._LazyStrategy__representation is None ): strategy._LazyStrategy__args = tuple( _strip_typevars(a) for a in strategy._LazyStrategy__args ) # Return a syntactically-valid strategy repr, including fixing some # strategy reprs and replacing invalid syntax reprs with `"nothing()"`. # String-replace to hide the special case in from_type() for Decimal('snan') r = ( repr(strategy) .replace(".filter(_can_hash)", "") .replace("hypothesis.strategies.", "") ) # Replace with ... in confusing lambdas r = re.sub(r"(lambda.*?: )()([,)])", r"\1...\3", r) compile(r, "", "eval") # Finally, try to work out the imports we need for builds(), .map(), # .filter(), and .flatmap() to work without NameError imports = {i for i in _imports_for_strategy(strategy) if i[1] in r} return imports, r except (SyntaxError, RecursionError, InvalidArgument): return set(), "nothing()" # When we ghostwrite for a module, we want to treat that as the __module__ for # each function, rather than whichever internal file it was actually defined in. KNOWN_FUNCTION_LOCATIONS: dict[object, str] = {} def _get_module_helper(obj): # Get the __module__ attribute of the object, and return the first ancestor module # which contains the object; falling back to the literal __module__ if none do. # The goal is to show location from which obj should usually be accessed, rather # than what we assume is an internal submodule which defined it. module_name = obj.__module__ # if "collections.abc" is used don't use the deprecated aliases in "collections" if module_name == "collections.abc": return module_name dots = [i for i, c in enumerate(module_name) if c == "."] + [None] for idx in dots: for candidate in (module_name[:idx].lstrip("_"), module_name[:idx]): if getattr(sys.modules.get(candidate), obj.__name__, None) is obj: KNOWN_FUNCTION_LOCATIONS[obj] = candidate return candidate return module_name def _get_module(obj): if obj in KNOWN_FUNCTION_LOCATIONS: return KNOWN_FUNCTION_LOCATIONS[obj] try: return _get_module_helper(obj) except AttributeError: if not _is_probably_ufunc(obj): raise for module_name in sorted(sys.modules, key=lambda n: tuple(n.split("."))): if obj is getattr(sys.modules[module_name], obj.__name__, None): KNOWN_FUNCTION_LOCATIONS[obj] = module_name return module_name raise RuntimeError(f"Could not find module for ufunc {obj.__name__} ({obj!r}") def _get_qualname(obj: Any, *, include_module: bool = False) -> str: # Replacing angle-brackets for objects defined in `..` qname = getattr(obj, "__qualname__", obj.__name__) qname = qname.replace("<", "_").replace(">", "_").replace(" ", "") if include_module: return _get_module(obj) + "." + qname return qname def _write_call( func: Callable, *pass_variables: str, except_: Except = Exception, assign: str = "" ) -> str: """Write a call to `func` with explicit and implicit arguments. >>> _write_call(sorted, "my_seq", "func") "builtins.sorted(my_seq, key=func, reverse=reverse)" >>> write_call(f, assign="var1") "var1 = f()" The fancy part is that we'll check the docstring for any known exceptions which `func` might raise, and catch-and-reject on them... *unless* they're subtypes of `except_`, which will be handled in an outer try-except block. """ args = ", ".join( ( (v or p.name) if p.kind is inspect.Parameter.POSITIONAL_ONLY else f"{p.name}={v or p.name}" ) for v, p in zip_longest(pass_variables, _get_params(func).values()) ) call = f"{_get_qualname(func, include_module=True)}({args})" if assign: call = f"{assign} = {call}" raises = _exceptions_from_docstring(getattr(func, "__doc__", "") or "") exnames = [ex.__name__ for ex in raises if not issubclass(ex, except_)] if not exnames: return call return SUPPRESS_BLOCK.format( test_body=indent(call, prefix=" "), exceptions="(" + ", ".join(exnames) + ")" if len(exnames) > 1 else exnames[0], ) def _st_strategy_names(s: str) -> str: """Replace strategy name() with st.name(). Uses a tricky re.sub() to avoid problems with frozensets() matching sets() too. """ names = "|".join(sorted(st.__all__, key=len, reverse=True)) return re.sub(pattern=rf"\b(?:{names})\b[^= ]", repl=r"st.\g<0>", string=s) def _make_test_body( *funcs: Callable, ghost: str, test_body: str, except_: tuple[type[Exception], ...], assertions: str = "", style: str, given_strategies: Mapping[str, str | st.SearchStrategy] | None = None, imports: ImportSet | None = None, annotate: bool, ) -> tuple[ImportSet, str]: # A set of modules to import - we might add to this later. The import code # is written later, so we can have one import section for multiple magic() # test functions. imports = (imports or set()) | {_get_module(f) for f in funcs} # Get strategies for all the arguments to each function we're testing. with _with_any_registered(): given_strategies = given_strategies or _get_strategies( *funcs, pass_result_to_next_func=ghost in ("idempotent", "roundtrip") ) reprs = [((k, *_valid_syntax_repr(v))) for k, v in given_strategies.items()] imports = imports.union(*(imp for _, imp, _ in reprs)) given_args = ", ".join(f"{k}={v}" for k, _, v in reprs) given_args = _st_strategy_names(given_args) if except_: # Convert to strings, either builtin names or qualified names. imp, exc_string = _exception_string(except_) imports.update(imp) # And finally indent the existing test body into a try-except block # which catches these exceptions and calls `hypothesis.reject()`. test_body = SUPPRESS_BLOCK.format( test_body=indent(test_body, prefix=" "), exceptions=exc_string, ) if assertions: test_body = f"{test_body}\n{assertions}" # Indent our test code to form the body of a function or method. argnames = ["self"] if style == "unittest" else [] if annotate: argnames.extend(_annotate_args(given_strategies, funcs, imports)) else: argnames.extend(given_strategies) body = TEMPLATE.format( given_args=given_args, test_kind=ghost, func_name="_".join(_get_qualname(f).replace(".", "_") for f in funcs), arg_names=", ".join(argnames), return_annotation=" -> None" if annotate else "", test_body=indent(test_body, prefix=" "), ) # For unittest-style, indent method further into a class body if style == "unittest": imports.add("unittest") body = "class Test{}{}(unittest.TestCase):\n{}".format( ghost.title(), "".join(_get_qualname(f).replace(".", "").title() for f in funcs), indent(body, " "), ) return imports, body def _annotate_args( argnames: Iterable[str], funcs: Iterable[Callable], imports: ImportSet ) -> Iterable[str]: arg_parameters: defaultdict[str, set[Any]] = defaultdict(set) for func in funcs: try: params = tuple(get_signature(func, eval_str=True).parameters.values()) except Exception: # don't add parameters if the annotations could not be evaluated pass else: for key, param in _params_to_dict(params).items(): if param.annotation != inspect.Parameter.empty: arg_parameters[key].add(param.annotation) for argname in argnames: parameters = arg_parameters.get(argname) annotation = _parameters_to_annotation_name(parameters, imports) if annotation is None: yield argname else: yield f"{argname}: {annotation}" class _AnnotationData(NamedTuple): type_name: str imports: set[str] def _parameters_to_annotation_name( parameters: Iterable[Any] | None, imports: ImportSet ) -> str | None: if parameters is None: return None annotations = tuple( annotation for annotation in map(_parameter_to_annotation, parameters) if annotation is not None ) if not annotations: return None if len(annotations) == 1: type_name, new_imports = annotations[0] imports.update(new_imports) return type_name joined = _join_generics(("typing.Union", {"typing"}), annotations) if joined is None: return None imports.update(joined.imports) return joined.type_name def _join_generics( origin_type_data: tuple[str, set[str]] | None, annotations: Iterable[_AnnotationData | None], ) -> _AnnotationData | None: if origin_type_data is None: return None # because typing.Optional is converted to a Union, it also contains None # since typing.Optional only accepts one type variable, we need to remove it if origin_type_data is not None and origin_type_data[0] == "typing.Optional": annotations = ( annotation for annotation in annotations if annotation is None or annotation.type_name != "None" ) origin_type, imports = origin_type_data joined = _join_argument_annotations(annotations) if joined is None or not joined[0]: return None arg_types, new_imports = joined imports.update(new_imports) return _AnnotationData("{}[{}]".format(origin_type, ", ".join(arg_types)), imports) def _join_argument_annotations( annotations: Iterable[_AnnotationData | None], ) -> tuple[list[str], set[str]] | None: imports: set[str] = set() arg_types: list[str] = [] for annotation in annotations: if annotation is None: return None arg_types.append(annotation.type_name) imports.update(annotation.imports) return arg_types, imports def _parameter_to_annotation(parameter: Any) -> _AnnotationData | None: # if a ForwardRef could not be resolved if isinstance(parameter, str): return None if isinstance(parameter, ForwardRef): if sys.version_info[:2] < (3, 14): forwarded_value = parameter.__forward_value__ if forwarded_value is None: return None else: # ForwardRef.__forward_value__ was removed in 3.14 in favor of # ForwardRef.evaluate(). See also PEP 649, PEP 749, and # typing.evaluate_forward_ref. # # .evaluate() with Format.VALUE (the default) throws if the name # could not be resolved. # https://docs.python.org/3.14/library/annotationlib.html#annotationlib.ForwardRef.evaluate try: forwarded_value = parameter.evaluate() except Exception: return None return _parameter_to_annotation(forwarded_value) # the arguments of Callable are in a list if isinstance(parameter, list): joined = _join_argument_annotations( _parameter_to_annotation(param) for param in parameter ) if joined is None: return None arg_type_names, new_imports = joined return _AnnotationData("[{}]".format(", ".join(arg_type_names)), new_imports) if isinstance(parameter, type): if parameter.__module__ == "builtins": return _AnnotationData( "None" if parameter.__name__ == "NoneType" else parameter.__name__, set(), ) type_name = _get_qualname(parameter, include_module=True) # the types.UnionType does not support type arguments and needs to be translated if type_name == "types.UnionType": return _AnnotationData("typing.Union", {"typing"}) else: if hasattr(parameter, "__module__") and hasattr(parameter, "__name__"): type_name = _get_qualname(parameter, include_module=True) else: type_name = str(parameter) if type_name.startswith("hypothesis.strategies."): return _AnnotationData(type_name.replace("hypothesis.strategies", "st"), set()) origin_type = get_origin(parameter) # if not generic or no generic arguments if origin_type is None or origin_type == parameter: return _AnnotationData(type_name, set(type_name.rsplit(".", maxsplit=1)[:-1])) arg_types = get_args(parameter) if {type(a) for a in arg_types} == {TypeVar}: arg_types = () # typing types get translated to classes that don't support generics origin_annotation: _AnnotationData | None if type_name.startswith("typing."): try: new_type_name = type_name[: type_name.index("[")] except ValueError: new_type_name = type_name origin_annotation = _AnnotationData(new_type_name, {"typing"}) else: origin_annotation = _parameter_to_annotation(origin_type) if arg_types: return _join_generics( origin_annotation, (_parameter_to_annotation(arg_type) for arg_type in arg_types), ) return origin_annotation def _are_annotations_used(*functions: Callable) -> bool: for function in functions: try: params = get_signature(function).parameters.values() except Exception: pass else: if any(param.annotation != inspect.Parameter.empty for param in params): return True return False def _make_test(imports: ImportSet, body: str) -> str: # Discarding "builtins." and "__main__" probably isn't particularly useful # for user code, but important for making a good impression in demos. body = body.replace("builtins.", "").replace("__main__.", "") imports |= {("hypothesis", "given"), ("hypothesis", "strategies as st")} if " reject()\n" in body: imports.add(("hypothesis", "reject")) do_not_import = {"builtins", "__main__", "hypothesis.strategies"} direct = {f"import {i}" for i in imports - do_not_import if isinstance(i, str)} from_imports = defaultdict(set) for module, name in {i for i in imports if isinstance(i, tuple)}: if not (module.startswith("hypothesis.strategies") and name in st.__all__): from_imports[module].add(name) from_ = { "from {} import {}".format(module, ", ".join(sorted(names))) for module, names in from_imports.items() if isinstance(module, str) and module not in do_not_import } header = IMPORT_SECTION.format(imports="\n".join(sorted(direct) + sorted(from_))) nothings = body.count("st.nothing()") if nothings == 1: header += "# TODO: replace st.nothing() with an appropriate strategy\n\n" elif nothings >= 1: header += "# TODO: replace st.nothing() with appropriate strategies\n\n" return black.format_str(header + body, mode=black.Mode()) def _is_probably_ufunc(obj): # See https://numpy.org/doc/stable/reference/ufuncs.html - there doesn't seem # to be an upstream function to detect this, so we just guess. has_attributes = [ "nin", "nout", "nargs", "ntypes", "types", "identity", "signature", ] return callable(obj) and all(hasattr(obj, name) for name in has_attributes) # If we have a pair of functions where one name matches the regex and the second # is the result of formatting the template with matched groups, our magic() # ghostwriter will write a roundtrip test for them. Additional patterns welcome. ROUNDTRIP_PAIRS = ( # Defined prefix, shared postfix. The easy cases. (r"write(.+)", "read{}"), (r"save(.+)", "load{}"), (r"dump(.+)", "load{}"), (r"to(.+)", "from{}"), # Known stem, maybe matching prefixes, maybe matching postfixes. (r"(.*)en(.+)", "{}de{}"), # Shared postfix, prefix only on "inverse" function (r"(.+)", "de{}"), (r"(?!safe)(.+)", "un{}"), # safe_load / unsafe_load isn't a roundtrip # a2b_postfix and b2a_postfix. Not a fan of this pattern, but it's pretty # common in code imitating an C API - see e.g. the stdlib binascii module. (r"(.+)2(.+?)(_.+)?", "{1}2{0}{2}"), # Common in e.g. the colorsys module (r"(.+)_to_(.+)", "{1}_to_{0}"), # Sockets patterns (r"(inet|if)_(.+)to(.+)", "{0}_{2}to{1}"), (r"(\w)to(\w)(.+)", "{1}to{0}{2}"), (r"send(.+)", "recv{}"), (r"send(.+)", "receive{}"), ) def _get_testable_functions(thing: object) -> dict[str, Callable]: by_name = {} if callable(thing): funcs: list[Any | None] = [thing] elif isinstance(thing, types.ModuleType): if hasattr(thing, "__all__"): funcs = [getattr(thing, name, None) for name in thing.__all__] elif hasattr(thing, "__package__"): pkg = thing.__package__ funcs = [ v for k, v in vars(thing).items() if callable(v) and not is_mock(v) and ((not pkg) or getattr(v, "__module__", pkg).startswith(pkg)) and not k.startswith("_") ] if pkg and any(getattr(f, "__module__", pkg) == pkg for f in funcs): funcs = [f for f in funcs if getattr(f, "__module__", pkg) == pkg] else: raise InvalidArgument(f"Can't test non-module non-callable {thing!r}") for f in list(funcs): if inspect.isclass(f): funcs += [ v.__get__(f) for k, v in vars(f).items() if hasattr(v, "__func__") and not is_mock(v) and not k.startswith("_") ] for f in funcs: try: if ( (not is_mock(f)) and callable(f) and _get_params(f) and not isinstance(f, enum.EnumMeta) ): if getattr(thing, "__name__", None): if inspect.isclass(thing): KNOWN_FUNCTION_LOCATIONS[f] = _get_module_helper(thing) elif isinstance(thing, types.ModuleType): KNOWN_FUNCTION_LOCATIONS[f] = thing.__name__ try: _get_params(f) by_name[_get_qualname(f, include_module=True)] = f except Exception: # usually inspect.signature on C code such as socket.inet_aton, # or Pandas 'CallableDynamicDoc' object has no attr. '__name__' pass except (TypeError, ValueError): pass return by_name def magic( *modules_or_functions: Callable | types.ModuleType, except_: Except = (), style: str = "pytest", annotate: bool | None = None, ) -> str: """Guess which ghostwriters to use, for a module or collection of functions. As for all ghostwriters, the ``except_`` argument should be an :class:`python:Exception` or tuple of exceptions, and ``style`` may be either ``"pytest"`` to write test functions or ``"unittest"`` to write test methods and :class:`~python:unittest.TestCase`. After finding the public functions attached to any modules, the ``magic`` ghostwriter looks for pairs of functions to pass to :func:`~roundtrip`, then checks for :func:`~binary_operation` and :func:`~ufunc` functions, and any others are passed to :func:`~fuzz`. For example, try :command:`hypothesis write gzip` on the command line! """ except_ = _check_except(except_) _check_style(style) if not modules_or_functions: raise InvalidArgument("Must pass at least one function or module to test.") parts = [] by_name = {} imports = set() for thing in modules_or_functions: by_name.update(found := _get_testable_functions(thing)) if (not found) and isinstance(thing, types.ModuleType): msg = f"# Found no testable functions in {thing.__name__} (from {thing.__file__!r})" mods: list = [] for k in sorted(sys.modules, key=len): if ( k.startswith(f"{thing.__name__}.") and "._" not in k.removeprefix(thing.__name__) and not k.startswith(tuple(f"{m}." for m in mods)) and _get_testable_functions(sys.modules[k]) ): mods.append(k) if mods: msg += ( f"\n# Try writing tests for submodules, e.g. by using:\n" f"# hypothesis write {' '.join(sorted(mods))}" ) parts.append(msg) if not by_name: return "\n\n".join(parts) if annotate is None: annotate = _are_annotations_used(*by_name.values()) def make_(how, *args, **kwargs): imp, body = how(*args, **kwargs, except_=except_, style=style) imports.update(imp) parts.append(body) # Look for pairs of functions that roundtrip, based on known naming patterns. for writename, readname in ROUNDTRIP_PAIRS: for name in sorted(by_name): match = re.fullmatch(writename, name.split(".")[-1]) if match: inverse_name = readname.format(*match.groups()) for other in sorted( n for n in by_name if n.split(".")[-1] == inverse_name ): make_( _make_roundtrip_body, (by_name.pop(name), by_name.pop(other)), annotate=annotate, ) break else: try: other_func = getattr( sys.modules[_get_module(by_name[name])], inverse_name, ) _get_params(other_func) # we want to skip if this fails except Exception: pass else: make_( _make_roundtrip_body, (by_name.pop(name), other_func), annotate=annotate, ) # Look for equivalent functions: same name, all required arguments of any can # be found in all signatures, and if all have return-type annotations they match. names = defaultdict(list) for _, f in sorted(by_name.items()): names[_get_qualname(f)].append(f) for group in names.values(): if len(group) >= 2 and len({frozenset(_get_params(f)) for f in group}) == 1: sentinel = object() returns = {get_type_hints(f).get("return", sentinel) for f in group} if len(returns - {sentinel}) <= 1: make_(_make_equiv_body, group, annotate=annotate) for f in group: by_name.pop(_get_qualname(f, include_module=True)) # Look for binary operators - functions with two identically-typed arguments, # and the same return type. The latter restriction might be lifted later. for name, func in sorted(by_name.items()): hints = get_type_hints(func) hints.pop("return", None) params = _get_params(func) if (len(hints) == len(params) == 2) or ( _get_module(func) == "operator" and "item" not in func.__name__ and tuple(params) in [("a", "b"), ("x", "y")] ): a, b = hints.values() or [Any, Any] arg1, arg2 = params if a == b and len(arg1) == len(arg2) <= 3: # https://en.wikipedia.org/wiki/Distributive_property#Other_examples known = { "mul": "add", "matmul": "add", "or_": "and_", "and_": "or_", }.get(func.__name__, "") distributes_over = getattr(sys.modules[_get_module(func)], known, None) make_( _make_binop_body, func, commutative=func.__name__ != "matmul", distributes_over=distributes_over, annotate=annotate, ) del by_name[name] # Look for Numpy ufuncs or gufuncs, and write array-oriented tests for them. if "numpy" in sys.modules: for name, func in sorted(by_name.items()): if _is_probably_ufunc(func): make_(_make_ufunc_body, func, annotate=annotate) del by_name[name] # For all remaining callables, just write a fuzz-test. In principle we could # guess at equivalence or idempotence; but it doesn't seem accurate enough to # be worth the trouble when it's so easy for the user to specify themselves. for _, f in sorted(by_name.items()): make_( _make_test_body, f, test_body=_write_call(f, except_=except_), ghost="fuzz", annotate=annotate, ) return _make_test(imports, "\n".join(parts)) def fuzz( func: Callable, *, except_: Except = (), style: str = "pytest", annotate: bool | None = None, ) -> str: """Write source code for a property-based test of ``func``. The resulting test checks that valid input only leads to expected exceptions. For example: .. code-block:: python from re import compile, error from hypothesis.extra import ghostwriter ghostwriter.fuzz(compile, except_=error) Gives: .. code-block:: python # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import re from hypothesis import given, reject, strategies as st # TODO: replace st.nothing() with an appropriate strategy @given(pattern=st.nothing(), flags=st.just(0)) def test_fuzz_compile(pattern, flags): try: re.compile(pattern=pattern, flags=flags) except re.error: reject() Note that it includes all the required imports. Because the ``pattern`` parameter doesn't have annotations or a default argument, you'll need to specify a strategy - for example :func:`~hypothesis.strategies.text` or :func:`~hypothesis.strategies.binary`. After that, you have a test! """ if not callable(func): raise InvalidArgument(f"Got non-callable {func=}") except_ = _check_except(except_) _check_style(style) if annotate is None: annotate = _are_annotations_used(func) imports, body = _make_test_body( func, test_body=_write_call(func, except_=except_), except_=except_, ghost="fuzz", style=style, annotate=annotate, ) return _make_test(imports, body) def idempotent( func: Callable, *, except_: Except = (), style: str = "pytest", annotate: bool | None = None, ) -> str: """Write source code for a property-based test of ``func``. The resulting test checks that if you call ``func`` on it's own output, the result does not change. For example: .. code-block:: python from typing import Sequence from hypothesis.extra import ghostwriter def timsort(seq: Sequence[int]) -> Sequence[int]: return sorted(seq) ghostwriter.idempotent(timsort) Gives: .. code-block:: python # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. from hypothesis import given, strategies as st @given(seq=st.one_of(st.binary(), st.binary().map(bytearray), st.lists(st.integers()))) def test_idempotent_timsort(seq): result = timsort(seq=seq) repeat = timsort(seq=result) assert result == repeat, (result, repeat) """ if not callable(func): raise InvalidArgument(f"Got non-callable {func=}") except_ = _check_except(except_) _check_style(style) if annotate is None: annotate = _are_annotations_used(func) imports, body = _make_test_body( func, test_body="result = {}\nrepeat = {}".format( _write_call(func, except_=except_), _write_call(func, "result", except_=except_), ), except_=except_, assertions=_assert_eq(style, "result", "repeat"), ghost="idempotent", style=style, annotate=annotate, ) return _make_test(imports, body) def _make_roundtrip_body(funcs, except_, style, annotate): first_param = next(iter(_get_params(funcs[0]))) test_lines = [ _write_call(funcs[0], assign="value0", except_=except_), *( _write_call(f, f"value{i}", assign=f"value{i + 1}", except_=except_) for i, f in enumerate(funcs[1:]) ), ] return _make_test_body( *funcs, test_body="\n".join(test_lines), except_=except_, assertions=_assert_eq(style, first_param, f"value{len(funcs) - 1}"), ghost="roundtrip", style=style, annotate=annotate, ) def roundtrip( *funcs: Callable, except_: Except = (), style: str = "pytest", annotate: bool | None = None, ) -> str: """Write source code for a property-based test of ``funcs``. The resulting test checks that if you call the first function, pass the result to the second (and so on), the final result is equal to the first input argument. This is a *very* powerful property to test, especially when the config options are varied along with the object to round-trip. For example, try ghostwriting a test for :func:`python:json.dumps` - would you have thought of all that? .. code-block:: shell hypothesis write --roundtrip json.dumps json.loads """ if not funcs: raise InvalidArgument("Round-trip of zero functions is meaningless.") for i, f in enumerate(funcs): if not callable(f): raise InvalidArgument(f"Got non-callable funcs[{i}]={f!r}") except_ = _check_except(except_) _check_style(style) if annotate is None: annotate = _are_annotations_used(*funcs) return _make_test(*_make_roundtrip_body(funcs, except_, style, annotate)) def _get_varnames(funcs): var_names = [f"result_{f.__name__}" for f in funcs] if len(set(var_names)) < len(var_names): var_names = [f"result_{f.__name__}_{_get_module(f)}" for f in funcs] if len(set(var_names)) < len(var_names): var_names = [f"result_{i}_{f.__name__}" for i, f in enumerate(funcs)] return var_names def _make_equiv_body(funcs, except_, style, annotate): var_names = _get_varnames(funcs) test_lines = [ _write_call(f, assign=vname, except_=except_) for vname, f in zip(var_names, funcs, strict=True) ] assertions = "\n".join( _assert_eq(style, var_names[0], vname) for vname in var_names[1:] ) return _make_test_body( *funcs, test_body="\n".join(test_lines), except_=except_, assertions=assertions, ghost="equivalent", style=style, annotate=annotate, ) EQUIV_FIRST_BLOCK = """ try: {} exc_type = None target(1, label="input was valid") {}except Exception as exc: exc_type = type(exc) """.strip() EQUIV_CHECK_BLOCK = """ if exc_type: with {ctx}(exc_type): {check_raises} else: {call} {compare} """.rstrip() def _make_equiv_errors_body(funcs, except_, style, annotate): var_names = _get_varnames(funcs) first, *rest = funcs first_call = _write_call(first, assign=var_names[0], except_=except_) extra_imports, suppress = _exception_string(except_) extra_imports.add(("hypothesis", "target")) catch = f"except {suppress}:\n reject()\n" if suppress else "" test_lines = [EQUIV_FIRST_BLOCK.format(indent(first_call, prefix=" "), catch)] for vname, f in zip(var_names[1:], rest, strict=True): if style == "pytest": ctx = "pytest.raises" extra_imports.add("pytest") else: assert style == "unittest" ctx = "self.assertRaises" block = EQUIV_CHECK_BLOCK.format( ctx=ctx, check_raises=indent(_write_call(f, except_=()), " "), call=indent(_write_call(f, assign=vname, except_=()), " "), compare=indent(_assert_eq(style, var_names[0], vname), " "), ) test_lines.append(block) imports, source_code = _make_test_body( *funcs, test_body="\n".join(test_lines), except_=(), ghost="equivalent", style=style, annotate=annotate, ) return imports | extra_imports, source_code def equivalent( *funcs: Callable, allow_same_errors: bool = False, except_: Except = (), style: str = "pytest", annotate: bool | None = None, ) -> str: """Write source code for a property-based test of ``funcs``. The resulting test checks that calling each of the functions returns an equal value. This can be used as a classic 'oracle', such as testing a fast sorting algorithm against the :func:`python:sorted` builtin, or for differential testing where none of the compared functions are fully trusted but any difference indicates a bug (e.g. running a function on different numbers of threads, or simply multiple times). The functions should have reasonably similar signatures, as only the common parameters will be passed the same arguments - any other parameters will be allowed to vary. If allow_same_errors is True, then the test will pass if calling each of the functions returns an equal value, *or* if the first function raises an exception and each of the others raises an exception of the same type. This relaxed mode can be useful for code synthesis projects. """ if len(funcs) < 2: raise InvalidArgument("Need at least two functions to compare.") for i, f in enumerate(funcs): if not callable(f): raise InvalidArgument(f"Got non-callable funcs[{i}]={f!r}") check_type(bool, allow_same_errors, "allow_same_errors") except_ = _check_except(except_) _check_style(style) if annotate is None: annotate = _are_annotations_used(*funcs) if allow_same_errors and not any(issubclass(Exception, ex) for ex in except_): imports, source_code = _make_equiv_errors_body(funcs, except_, style, annotate) else: imports, source_code = _make_equiv_body(funcs, except_, style, annotate) return _make_test(imports, source_code) X = TypeVar("X") Y = TypeVar("Y") def binary_operation( func: Callable[[X, X], Y], *, associative: bool = True, commutative: bool = True, identity: X | EllipsisType | None = ..., distributes_over: Callable[[X, X], X] | None = None, except_: Except = (), style: str = "pytest", annotate: bool | None = None, ) -> str: """Write property tests for the binary operation ``func``. While :wikipedia:`binary operations ` are not particularly common, they have such nice properties to test that it seems a shame not to demonstrate them with a ghostwriter. For an operator ``f``, test that: - if :wikipedia:`associative `, ``f(a, f(b, c)) == f(f(a, b), c)`` - if :wikipedia:`commutative `, ``f(a, b) == f(b, a)`` - if :wikipedia:`identity ` is not None, ``f(a, identity) == a`` - if :wikipedia:`distributes_over ` is ``+``, ``f(a, b) + f(a, c) == f(a, b+c)`` For example: .. code-block:: python ghostwriter.binary_operation( operator.mul, identity=1, distributes_over=operator.add, style="unittest", ) """ if not callable(func): raise InvalidArgument(f"Got non-callable {func=}") except_ = _check_except(except_) _check_style(style) check_type(bool, associative, "associative") check_type(bool, commutative, "commutative") if distributes_over is not None and not callable(distributes_over): raise InvalidArgument( f"{distributes_over=} must be an operation which " f"distributes over {func.__name__}" ) if not any([associative, commutative, identity, distributes_over]): raise InvalidArgument( "You must select at least one property of the binary operation to test." ) if annotate is None: annotate = _are_annotations_used(func) imports, body = _make_binop_body( func, associative=associative, commutative=commutative, identity=identity, distributes_over=distributes_over, except_=except_, style=style, annotate=annotate, ) return _make_test(imports, body) def _make_binop_body( func: Callable[[X, X], Y], *, associative: bool = True, commutative: bool = True, identity: X | EllipsisType | None = ..., distributes_over: Callable[[X, X], X] | None = None, except_: tuple[type[Exception], ...], style: str, annotate: bool, ) -> tuple[ImportSet, str]: strategies = _get_strategies(func) operands, b = (strategies.pop(p) for p in list(_get_params(func))[:2]) if repr(operands) != repr(b): operands |= b operands_name = func.__name__ + "_operands" all_imports = set() parts = [] def maker( sub_property: str, args: str, body: str, right: str | None = None, ) -> None: if right is None: assertions = "" else: body = f"{body}\n{right}" assertions = _assert_eq(style, "left", "right") imports, body = _make_test_body( func, test_body=body, ghost=sub_property + "_binary_operation", except_=except_, assertions=assertions, style=style, given_strategies={**strategies, **{n: operands_name for n in args}}, annotate=annotate, ) all_imports.update(imports) if style == "unittest": endline = "(unittest.TestCase):\n" body = body[body.index(endline) + len(endline) + 1 :] parts.append(body) if associative: maker( "associative", "abc", _write_call(func, "a", _write_call(func, "b", "c"), assign="left"), _write_call( func, _write_call(func, "a", "b"), "c", assign="right", ), ) if commutative: maker( "commutative", "ab", _write_call(func, "a", "b", assign="left"), _write_call(func, "b", "a", assign="right"), ) if identity is not None: # Guess that the identity element is the minimal example from our operands # strategy. This is correct often enough to be worthwhile, and close enough # that it's a good starting point to edit much of the rest. if identity is ...: try: identity = find(operands, lambda x: True, settings=_quietly_settings) except Exception: identity = "identity element here" # type: ignore # If the repr of this element is invalid Python, stringify it - this # can't be executed as-is, but at least makes it clear what should # happen. E.g. type(None) -> -> quoted. try: # We don't actually execute this code object; we're just compiling # to check that the repr is syntactically valid. HOWEVER, we're # going to output that code string into test code which will be # executed; so you still shouldn't ghostwrite for hostile code. compile(repr(identity), "", "exec") except SyntaxError: identity = repr(identity) # type: ignore identity_parts = [ f"{identity = }", _assert_eq( style, "a", _write_call(func, "a", "identity"), ), _assert_eq( style, "a", _write_call(func, "identity", "a"), ), ] maker("identity", "a", "\n".join(identity_parts)) if distributes_over: do = distributes_over dist_parts = [ _write_call(func, "a", _write_call(do, "b", "c"), assign="left"), _write_call( do, _write_call(func, "a", "b"), _write_call(func, "a", "c"), assign="ldist", ), _assert_eq(style, "ldist", "left"), "\n", _write_call(func, _write_call(do, "a", "b"), "c", assign="right"), _write_call( do, _write_call(func, "a", "c"), _write_call(func, "b", "c"), assign="rdist", ), _assert_eq(style, "rdist", "right"), ] maker(do.__name__ + "_distributes_over", "abc", "\n".join(dist_parts)) operands_imports, operands_repr = _valid_syntax_repr(operands) all_imports.update(operands_imports) operands_repr = _st_strategy_names(operands_repr) classdef = "" if style == "unittest": classdef = f"class TestBinaryOperation{func.__name__}(unittest.TestCase):\n " return ( all_imports, classdef + f"{operands_name} = {operands_repr}\n" + "\n".join(parts), ) def ufunc( func: Callable, *, except_: Except = (), style: str = "pytest", annotate: bool | None = None, ) -> str: """Write a property-based test for the :doc:`array ufunc ` ``func``. The resulting test checks that your ufunc or :doc:`gufunc ` has the expected broadcasting and dtype casting behaviour. You will probably want to add extra assertions, but as with the other ghostwriters this gives you a great place to start. .. code-block:: shell hypothesis write numpy.matmul """ if not _is_probably_ufunc(func): raise InvalidArgument(f"{func=} does not seem to be a ufunc") except_ = _check_except(except_) _check_style(style) if annotate is None: annotate = _are_annotations_used(func) return _make_test( *_make_ufunc_body(func, except_=except_, style=style, annotate=annotate) ) def _make_ufunc_body(func, *, except_, style, annotate): import hypothesis.extra.numpy as npst if func.signature is None: shapes = npst.mutually_broadcastable_shapes(num_shapes=func.nin) else: shapes = npst.mutually_broadcastable_shapes(signature=func.signature) shapes.function.__module__ = npst.__name__ body = """ input_shapes, expected_shape = shapes input_dtypes, expected_dtype = types.split("->") array_strats = [ arrays(dtype=dtp, shape=shp, elements={{"allow_nan": True}}) for dtp, shp in zip(input_dtypes, input_shapes) ] {array_names} = data.draw(st.tuples(*array_strats)) result = {call} """.format( array_names=", ".join(ascii_lowercase[: func.nin]), call=_write_call(func, *ascii_lowercase[: func.nin], except_=except_), ) assertions = "\n{shape_assert}\n{type_assert}".format( shape_assert=_assert_eq(style, "result.shape", "expected_shape"), type_assert=_assert_eq(style, "result.dtype.char", "expected_dtype"), ) qname = _get_qualname(func, include_module=True) obj_sigs = ["O" in sig for sig in func.types] if all(obj_sigs) or not any(obj_sigs): types = f"sampled_from({qname}.types)" else: types = f"sampled_from([sig for sig in {qname}.types if 'O' not in sig])" return _make_test_body( func, test_body=dedent(body).strip(), except_=except_, assertions=assertions, ghost="ufunc" if func.signature is None else "gufunc", style=style, given_strategies={"data": st.data(), "shapes": shapes, "types": types}, imports={("hypothesis.extra.numpy", "arrays")}, annotate=annotate, ) ================================================ FILE: hypothesis-python/src/hypothesis/extra/lark.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ This extra can be used to generate strings matching any context-free grammar, using the `Lark parser library `_. It currently only supports Lark's native EBNF syntax, but we plan to extend this to support other common syntaxes such as ANTLR and :rfc:`5234` ABNF. Lark already `supports loading grammars `_ from `nearley.js `_, so you may not have to write your own at all. """ from inspect import signature import lark from lark.grammar import NonTerminal, Rule, Symbol, Terminal from lark.lark import Lark from lark.lexer import TerminalDef from hypothesis import strategies as st from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.utils import calc_label_from_name from hypothesis.internal.validation import check_type from hypothesis.strategies._internal.regex import IncompatibleWithAlphabet from hypothesis.strategies._internal.utils import cacheable, defines_strategy __all__ = ["from_lark"] def get_terminal_names( terminals: list[TerminalDef], rules: list[Rule], ignore_names: list[str] ) -> set[str]: """Get names of all terminals in the grammar. The arguments are the results of calling ``Lark.grammar.compile()``, so you would think that the ``terminals`` and ``ignore_names`` would have it all... but they omit terminals created with ``@declare``, which appear only in the expansion(s) of nonterminals. """ names = {t.name for t in terminals} | set(ignore_names) for rule in rules: names |= {t.name for t in rule.expansion if isinstance(t, Terminal)} return names class LarkStrategy(st.SearchStrategy): """Low-level strategy implementation wrapping a Lark grammar. See ``from_lark`` for details. """ def __init__( self, grammar: Lark, start: str | None, explicit: dict[str, st.SearchStrategy[str]], alphabet: st.SearchStrategy[str], ) -> None: super().__init__() assert isinstance(grammar, lark.lark.Lark) start: list[str] = grammar.options.start if start is None else [start] # This is a total hack, but working around the changes is a nicer user # experience than breaking for anyone who doesn't instantly update their # installation of Lark alongside Hypothesis. compile_args = signature(grammar.grammar.compile).parameters if "terminals_to_keep" in compile_args: terminals, rules, ignore_names = grammar.grammar.compile(start, ()) elif "start" in compile_args: # pragma: no cover # Support lark <= 0.10.0, without the terminals_to_keep argument. terminals, rules, ignore_names = grammar.grammar.compile(start) # type: ignore else: # pragma: no cover # This branch is to support lark <= 0.7.1, without the start argument. terminals, rules, ignore_names = grammar.grammar.compile() # type: ignore self.names_to_symbols: dict[str, Symbol] = {} for r in rules: self.names_to_symbols[r.origin.name] = r.origin disallowed = set() self.terminal_strategies: dict[str, st.SearchStrategy[str]] = {} for t in terminals: self.names_to_symbols[t.name] = Terminal(t.name) s = st.from_regex(t.pattern.to_regexp(), fullmatch=True, alphabet=alphabet) try: s.validate() except IncompatibleWithAlphabet: disallowed.add(t.name) else: self.terminal_strategies[t.name] = s self.ignored_symbols = tuple(self.names_to_symbols[n] for n in ignore_names) all_terminals = get_terminal_names(terminals, rules, ignore_names) if unknown_explicit := sorted(set(explicit) - all_terminals): raise InvalidArgument( "The following arguments were passed as explicit_strategies, but " f"there is no {unknown_explicit} terminal production in this grammar." ) if missing_declared := sorted( all_terminals - {t.name for t in terminals} - set(explicit) ): raise InvalidArgument( f"Undefined terminal{'s' * (len(missing_declared) > 1)} " f"{sorted(missing_declared)!r}. Generation does not currently " "support use of %declare unless you pass `explicit`, a dict of " f"names-to-strategies, such as `{{{missing_declared[0]!r}: " 'st.just("")}}`' ) self.terminal_strategies.update(explicit) # can in fact contain any symbol, despite its name. nonterminals: dict[str, list[tuple[Symbol, ...]]] = {} for rule in rules: if disallowed.isdisjoint(r.name for r in rule.expansion): nonterminals.setdefault(rule.origin.name, []).append( tuple(rule.expansion) ) allowed_rules = {*self.terminal_strategies, *nonterminals} while dict(nonterminals) != ( nonterminals := { k: clean for k, v in nonterminals.items() if (clean := [x for x in v if all(r.name in allowed_rules for r in x)]) } ): allowed_rules = {*self.terminal_strategies, *nonterminals} if set(start).isdisjoint(allowed_rules): raise InvalidArgument( f"No start rule {tuple(start)} is allowed by {alphabet=}" ) self.start = st.sampled_from( [self.names_to_symbols[s] for s in start if s in allowed_rules] ) self.nonterminal_strategies = { k: st.sampled_from(sorted(v, key=len)) for k, v in nonterminals.items() } self.__rule_labels: dict[str, int] = {} def do_draw(self, data: ConjectureData) -> str: state: list[str] = [] start = data.draw(self.start) self.draw_symbol(data, start, state) return "".join(state) def rule_label(self, name: str) -> int: try: return self.__rule_labels[name] except KeyError: return self.__rule_labels.setdefault( name, calc_label_from_name(f"LARK:{name}") ) def draw_symbol( self, data: ConjectureData, symbol: Symbol, draw_state: list[str], ) -> None: if isinstance(symbol, Terminal): strategy = self.terminal_strategies[symbol.name] draw_state.append(data.draw(strategy)) else: assert isinstance(symbol, NonTerminal) data.start_span(self.rule_label(symbol.name)) expansion = data.draw(self.nonterminal_strategies[symbol.name]) for e in expansion: self.draw_symbol(data, e, draw_state) self.gen_ignore(data, draw_state) data.stop_span() def gen_ignore(self, data: ConjectureData, draw_state: list[str]) -> None: if self.ignored_symbols and data.draw_boolean(1 / 4): emit = data.draw(st.sampled_from(self.ignored_symbols)) self.draw_symbol(data, emit, draw_state) def calc_has_reusable_values(self, recur): return True def check_explicit(name): def inner(value): check_type(str, value, "value drawn from " + name) return value return inner @cacheable @defines_strategy(force_reusable_values=True) def from_lark( grammar: lark.lark.Lark, *, start: str | None = None, explicit: dict[str, st.SearchStrategy[str]] | None = None, alphabet: st.SearchStrategy[str] = st.characters(codec="utf-8"), ) -> st.SearchStrategy[str]: """A strategy for strings accepted by the given context-free grammar. ``grammar`` must be a ``Lark`` object, which wraps an EBNF specification. The Lark EBNF grammar reference can be found `here `_. ``from_lark`` will automatically generate strings matching the nonterminal ``start`` symbol in the grammar, which was supplied as an argument to the Lark class. To generate strings matching a different symbol, including terminals, you can override this by passing the ``start`` argument to ``from_lark``. Note that Lark may remove unreachable productions when the grammar is compiled, so you should probably pass the same value for ``start`` to both. Currently ``from_lark`` does not support grammars that need custom lexing. Any lexers will be ignored, and any undefined terminals from the use of ``%declare`` will result in generation errors. To define strategies for such terminals, pass a dictionary mapping their name to a corresponding strategy as the ``explicit`` argument. The :pypi:`hypothesmith` project includes a strategy for Python source, based on a grammar and careful post-processing. """ check_type(lark.lark.Lark, grammar, "grammar") if explicit is None: explicit = {} else: check_type(dict, explicit, "explicit") explicit = { k: v.map(check_explicit(f"explicit[{k!r}]={v!r}")) for k, v in explicit.items() } return LarkStrategy(grammar, start, explicit, alphabet) ================================================ FILE: hypothesis-python/src/hypothesis/extra/numpy.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import importlib import math import types from collections.abc import Mapping, Sequence from typing import ( TYPE_CHECKING, Any, Literal, TypeVar, Union, cast, get_args, get_origin, overload, ) import numpy as np from hypothesis import strategies as st from hypothesis.errors import HypothesisException, InvalidArgument from hypothesis.extra._array_helpers import ( _BIE, NDIM_MAX, BasicIndex, BasicIndexStrategy, BroadcastableShapes, Shape, _BIENoEllipsis, _BIENoEllipsisNoNewaxis, _BIENoNewaxis, array_shapes, broadcastable_shapes, check_argument, check_valid_dims, mutually_broadcastable_shapes as _mutually_broadcastable_shapes, order_check, valid_tuple_axes as _valid_tuple_axes, ) from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.coverage import check_function from hypothesis.internal.reflection import proxies from hypothesis.internal.validation import check_type from hypothesis.strategies._internal.lazy import unwrap_strategies from hypothesis.strategies._internal.numbers import Real from hypothesis.strategies._internal.strategies import ( Ex, MappedStrategy, T, check_strategy, ) from hypothesis.strategies._internal.utils import defines_strategy from hypothesis.utils.deprecation import note_deprecation def _try_import(mod_name: str, attr_name: str) -> Any: assert "." not in attr_name try: mod = importlib.import_module(mod_name) return getattr(mod, attr_name, None) except ImportError: return None if TYPE_CHECKING: from numpy.typing import DTypeLike, NDArray else: NDArray = _try_import("numpy.typing", "NDArray") ArrayLike = _try_import("numpy.typing", "ArrayLike") _NestedSequence = _try_import("numpy._typing._nested_sequence", "_NestedSequence") _SupportsArray = _try_import("numpy._typing._array_like", "_SupportsArray") __all__ = [ "BroadcastableShapes", "array_dtypes", "array_shapes", "arrays", "basic_indices", "boolean_dtypes", "broadcastable_shapes", "byte_string_dtypes", "complex_number_dtypes", "datetime64_dtypes", "floating_dtypes", "from_dtype", "integer_array_indices", "integer_dtypes", "mutually_broadcastable_shapes", "nested_dtypes", "scalar_dtypes", "timedelta64_dtypes", "unicode_string_dtypes", "unsigned_integer_dtypes", "valid_tuple_axes", ] TIME_RESOLUTIONS = ("Y", "M", "D", "h", "m", "s", "ms", "us", "ns", "ps", "fs", "as") # See https://github.com/HypothesisWorks/hypothesis/pull/3394 and linked discussion. NP_FIXED_UNICODE = tuple(int(x) for x in np.__version__.split(".")[:2]) >= (1, 19) @defines_strategy(force_reusable_values=True) def from_dtype( dtype: np.dtype, *, alphabet: st.SearchStrategy[str] | None = None, min_size: int = 0, max_size: int | None = None, min_value: int | float | None = None, max_value: int | float | None = None, allow_nan: bool | None = None, allow_infinity: bool | None = None, allow_subnormal: bool | None = None, exclude_min: bool | None = None, exclude_max: bool | None = None, min_magnitude: Real = 0, max_magnitude: Real | None = None, ) -> st.SearchStrategy[Any]: """Creates a strategy which can generate any value of the given dtype. Compatible parameters are passed to the inferred strategy function while inapplicable ones are ignored. This allows you, for example, to customise the min and max values, control the length or contents of strings, or exclude non-finite numbers. This is particularly useful when kwargs are passed through from :func:`arrays` which allow a variety of numeric dtypes, as it seamlessly handles the ``width`` or representable bounds for you. """ check_type(np.dtype, dtype, "dtype") kwargs = {k: v for k, v in locals().items() if k != "dtype" and v is not None} # Compound datatypes, eg 'f4,f4,f4' if dtype.names is not None and dtype.fields is not None: # mapping np.void.type over a strategy is nonsense, so return now. subs = [from_dtype(dtype.fields[name][0], **kwargs) for name in dtype.names] return st.tuples(*subs) # Subarray datatypes, eg '(2, 3)i4' if dtype.subdtype is not None: subtype, shape = dtype.subdtype return arrays(subtype, shape, elements=kwargs) def compat_kw(*args, **kw): """Update default args to the strategy with user-supplied keyword args.""" assert {"min_value", "max_value", "max_size"}.issuperset(kw) for key in set(kwargs).intersection(kw): msg = f"dtype {dtype!r} requires {key}={kwargs[key]!r} to be %s {kw[key]!r}" if kw[key] is not None: if key.startswith("min_") and kw[key] > kwargs[key]: raise InvalidArgument(msg % ("at least",)) elif key.startswith("max_") and kw[key] < kwargs[key]: raise InvalidArgument(msg % ("at most",)) kw.update({k: v for k, v in kwargs.items() if k in args or k in kw}) return kw # Scalar datatypes if dtype.kind == "b": result: st.SearchStrategy[Any] = st.booleans() elif dtype.kind == "f": result = st.floats( width=cast(Literal[16, 32, 64], min(8 * dtype.itemsize, 64)), **compat_kw( "min_value", "max_value", "allow_nan", "allow_infinity", "allow_subnormal", "exclude_min", "exclude_max", ), ) elif dtype.kind == "c": result = st.complex_numbers( width=cast( Literal[32, 64, 128], min(8 * dtype.itemsize, 128) ), # convert from bytes to bits **compat_kw( "min_magnitude", "max_magnitude", "allow_nan", "allow_infinity", "allow_subnormal", ), ) elif dtype.kind in ("S", "a"): # Numpy strings are null-terminated; only allow round-trippable values. # `itemsize == 0` means 'fixed length determined at array creation' max_size = dtype.itemsize or None result = st.binary(**compat_kw("min_size", max_size=max_size)).filter( lambda b: b[-1:] != b"\0" ) elif dtype.kind == "u": kw = compat_kw(min_value=0, max_value=2 ** (8 * dtype.itemsize) - 1) result = st.integers(**kw) elif dtype.kind == "i": overflow = 2 ** (8 * dtype.itemsize - 1) result = st.integers(**compat_kw(min_value=-overflow, max_value=overflow - 1)) elif dtype.kind == "U": # Encoded in UTF-32 (four bytes/codepoint) and null-terminated max_size = (dtype.itemsize or 0) // 4 or None if NP_FIXED_UNICODE and "alphabet" not in kwargs: kwargs["alphabet"] = st.characters() result = st.text(**compat_kw("alphabet", "min_size", max_size=max_size)).filter( lambda b: b[-1:] != "\0" ) elif dtype.kind in ("m", "M"): if "[" in dtype.str: res = st.just(dtype.str.split("[")[-1][:-1]) else: # Note that this case isn't valid to pass to arrays(), but we support # it here because we'd have to guard against equivalents in arrays() # regardless and drawing scalars is a valid use-case. res = st.sampled_from(TIME_RESOLUTIONS) if allow_nan is not False: elems = st.integers(-(2**63), 2**63 - 1) | st.just("NaT") else: # NEP-7 defines the NaT value as integer -(2**63) elems = st.integers(-(2**63) + 1, 2**63 - 1) result = st.builds(dtype.type, elems, res) elif dtype.kind == "O": return st.from_type(object) else: raise InvalidArgument(f"No strategy inference for {dtype}") return result.map(dtype.type) class ArrayStrategy(st.SearchStrategy): def __init__(self, element_strategy, shape, dtype, fill, unique): super().__init__() self.shape = tuple(shape) self.fill = fill self.array_size = int(np.prod(shape)) self.dtype = dtype self.element_strategy = element_strategy self.unique = unique self._check_elements = dtype.kind not in ("O", "V") def __repr__(self): return ( f"ArrayStrategy({self.element_strategy!r}, shape={self.shape}, " f"dtype={self.dtype!r}, fill={self.fill!r}, unique={self.unique!r})" ) def set_element(self, val, result, idx, *, fill=False): # `val` is either an arbitrary object (for dtype="O"), or otherwise an # instance of a numpy dtype. This means we can *usually* expect e.g. # val.dtype to be present, but can only guarantee it if # `self.dtype != "O"`. try: result[idx] = val except TypeError as err: raise InvalidArgument( f"Could not add element={val!r} of " f"{getattr(val, 'dtype', type(val))} to array of " f"{result.dtype!r} - possible mismatch of time units in dtypes?" ) from err try: elem_changed = self._check_elements and val != result[idx] and val == val except Exception as err: # pragma: no cover # This branch only exists to help debug weird behaviour in Numpy, # such as the string problems we had a while back. raise HypothesisException( f"Internal error when checking element={val!r} of " f"{getattr(val, 'dtype', type(val))!r} to array of " f"{result.dtype!r}" ) from err if elem_changed: strategy = self.fill if fill else self.element_strategy if self.dtype.kind == "f": # pragma: no cover # This logic doesn't trigger in our coverage tests under Numpy 1.24+, # with built-in checks for overflow, but we keep it for good error # messages and compatibility with older versions of Numpy. try: is_subnormal = 0 < abs(val) < np.finfo(self.dtype).tiny except Exception: # val may be a non-float that does not support the # operations __lt__ and __abs__ is_subnormal = False if is_subnormal: raise InvalidArgument( f"Generated subnormal float {val} from strategy " f"{strategy} resulted in {result[idx]!r}, probably " "as a result of NumPy being built with flush-to-zero " "compiler options. Consider passing " "allow_subnormal=False." ) raise InvalidArgument( f"Generated array element {val!r} from {strategy!r} cannot be " f"represented as dtype {self.dtype!r} - instead it becomes " f"{result[idx]!r} (type {type(result[idx])!r}). Consider using " "a more precise strategy, for example passing the `width` argument " "to `floats()`." ) def do_draw(self, data): if 0 in self.shape: return np.zeros(dtype=self.dtype, shape=self.shape) # Because Numpy allocates memory for strings at array creation, if we have # an unsized string dtype we'll fill an object array and then cast it back. unsized_string_dtype = ( self.dtype.kind in ("S", "a", "U") and self.dtype.itemsize == 0 ) # This could legitimately be a np.empty, but the performance gains for # that would be so marginal that there's really not much point risking # undefined behaviour shenanigans. result = np.zeros( shape=self.array_size, dtype=object if unsized_string_dtype else self.dtype ) if self.fill.is_empty: # We have no fill value (either because the user explicitly # disabled it or because the default behaviour was used and our # elements strategy does not produce reusable values), so we must # generate a fully dense array with a freshly drawn value for each # entry. if self.unique: elems = st.lists( self.element_strategy, min_size=self.array_size, max_size=self.array_size, unique=True, ) for i, v in enumerate(data.draw(elems)): self.set_element(v, result, i) else: for i in range(len(result)): self.set_element(data.draw(self.element_strategy), result, i) else: # We draw numpy arrays as "sparse with an offset". We draw a # collection of index assignments within the array and assign # fresh values from our elements strategy to those indices. If at # the end we have not assigned every element then we draw a single # value from our fill strategy and use that to populate the # remaining positions with that strategy. elements = cu.many( data, min_size=0, max_size=self.array_size, # sqrt isn't chosen for any particularly principled reason. It # just grows reasonably quickly but sublinearly, and for small # arrays it represents a decent fraction of the array size. average_size=min( 0.9 * self.array_size, # ensure small arrays sometimes use fill max(10, math.sqrt(self.array_size)), # ...but *only* sometimes ), ) needs_fill = np.full(self.array_size, True) seen = set() while elements.more(): i = data.draw_integer(0, self.array_size - 1) if not needs_fill[i]: elements.reject() continue self.set_element(data.draw(self.element_strategy), result, i) if self.unique: if result[i] in seen: elements.reject() continue seen.add(result[i]) needs_fill[i] = False if needs_fill.any(): # We didn't fill all of the indices in the early loop, so we # put a fill value into the rest. # We have to do this hilarious little song and dance to work # around numpy's special handling of iterable values. If the # value here were e.g. a tuple then neither array creation # nor putmask would do the right thing. But by creating an # array of size one and then assigning the fill value as a # single element, we both get an array with the right value in # it and putmask will do the right thing by repeating the # values of the array across the mask. one_element = np.zeros( shape=1, dtype=object if unsized_string_dtype else self.dtype ) self.set_element(data.draw(self.fill), one_element, 0, fill=True) if unsized_string_dtype: one_element = one_element.astype(self.dtype) fill_value = one_element[0] if self.unique: try: is_nan = np.isnan(fill_value) except TypeError: is_nan = False if not is_nan: raise InvalidArgument( f"Cannot fill unique array with non-NaN value {fill_value!r}" ) np.putmask(result, needs_fill, one_element) if unsized_string_dtype: out = result.astype(self.dtype) mismatch = out != result if mismatch.any(): raise InvalidArgument( f"Array elements {result[mismatch]!r} cannot be represented " f"as dtype {self.dtype!r} - instead they become " f"{out[mismatch]!r}. Use a more precise strategy, e.g. without " "trailing null bytes, as this will be an error future versions." ) result = out result = result.reshape(self.shape).copy() assert result.base is None return result def fill_for(elements, unique, fill, name=""): if fill is None: if unique or not elements.has_reusable_values: fill = st.nothing() else: fill = elements else: check_strategy(fill, f"{name}.fill" if name else "fill") return fill D = TypeVar("D", bound="DTypeLike") G = TypeVar("G", bound="np.generic") @overload def arrays( dtype: Union["np.dtype[G]", st.SearchStrategy["np.dtype[G]"]], shape: int | st.SearchStrategy[int] | Shape | st.SearchStrategy[Shape], *, elements: st.SearchStrategy[Any] | Mapping[str, Any] | None = None, fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> "st.SearchStrategy[NDArray[G]]": ... @overload def arrays( dtype: D | st.SearchStrategy[D], shape: int | st.SearchStrategy[int] | Shape | st.SearchStrategy[Shape], *, elements: st.SearchStrategy[Any] | Mapping[str, Any] | None = None, fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> "st.SearchStrategy[NDArray[Any]]": ... @defines_strategy(force_reusable_values=True) def arrays( dtype: D | st.SearchStrategy[D], shape: int | st.SearchStrategy[int] | Shape | st.SearchStrategy[Shape], *, elements: st.SearchStrategy[Any] | Mapping[str, Any] | None = None, fill: st.SearchStrategy[Any] | None = None, unique: bool = False, ) -> "st.SearchStrategy[NDArray[Any]]": r"""Returns a strategy for generating :class:`numpy:numpy.ndarray`\ s. * ``dtype`` may be any valid input to :class:`~numpy:numpy.dtype` (this includes :class:`~numpy:numpy.dtype` objects), or a strategy that generates such values. * ``shape`` may be an integer >= 0, a tuple of such integers, or a strategy that generates such values. * ``elements`` is a strategy for generating values to put in the array. If it is None a suitable value will be inferred based on the dtype, which may give any legal value (including eg NaN for floats). If a mapping, it will be passed as ``**kwargs`` to ``from_dtype()`` * ``fill`` is a strategy that may be used to generate a single background value for the array. If None, a suitable default will be inferred based on the other arguments. If set to :func:`~hypothesis.strategies.nothing` then filling behaviour will be disabled entirely and every element will be generated independently. * ``unique`` specifies if the elements of the array should all be distinct from one another. Note that in this case multiple NaN values may still be allowed. If fill is also set, the only valid values for it to return are NaN values (anything for which :obj:`numpy:numpy.isnan` returns True. So e.g. for complex numbers ``nan+1j`` is also a valid fill). Note that if ``unique`` is set to ``True`` the generated values must be hashable. Arrays of specified ``dtype`` and ``shape`` are generated for example like this: .. code-block:: pycon >>> import numpy as np >>> arrays(np.int8, (2, 3)).example() array([[-8, 6, 3], [-6, 4, 6]], dtype=int8) >>> arrays(np.float, 3, elements=st.floats(0, 1)).example() array([ 0.88974794, 0.77387938, 0.1977879 ]) Array values are generated in two parts: 1. Some subset of the coordinates of the array are populated with a value drawn from the elements strategy (or its inferred form). 2. If any coordinates were not assigned in the previous step, a single value is drawn from the ``fill`` strategy and is assigned to all remaining places. You can set :func:`fill=nothing() ` to disable this behaviour and draw a value for every element. If ``fill=None``, then it will attempt to infer the correct behaviour automatically. If ``unique`` is ``True``, no filling will occur by default. Otherwise, if it looks safe to reuse the values of elements across multiple coordinates (this will be the case for any inferred strategy, and for most of the builtins, but is not the case for mutable values or strategies built with flatmap, map, composite, etc) then it will use the elements strategy as the fill, else it will default to having no fill. Having a fill helps Hypothesis craft high quality examples, but its main importance is when the array generated is large: Hypothesis is primarily designed around testing small examples. If you have arrays with hundreds or more elements, having a fill value is essential if you want your tests to run in reasonable time. """ # Our dtype argument might be a union, e.g. `np.float64 | np.complex64`; we handle # that by turning it into a strategy up-front. if type(dtype) in (getattr(types, "UnionType", object()), Union): dtype = st.one_of(*(from_dtype(np.dtype(d)) for d in dtype.__args__)) # type: ignore # We support passing strategies as arguments for convenience, or at least # for legacy reasons, but don't want to pay the perf cost of a composite # strategy (i.e. repeated argument handling and validation) when it's not # needed. So we get the best of both worlds by recursing with flatmap, # but only when it's actually needed. if isinstance(dtype, st.SearchStrategy): return dtype.flatmap( lambda d: arrays(d, shape, elements=elements, fill=fill, unique=unique) ) if isinstance(shape, st.SearchStrategy): return shape.flatmap( lambda s: arrays(dtype, s, elements=elements, fill=fill, unique=unique) ) # From here on, we're only dealing with values and it's relatively simple. dtype = np.dtype(dtype) # type: ignore[arg-type] assert isinstance(dtype, np.dtype) # help mypy out a bit... if elements is None or isinstance(elements, Mapping): if dtype.kind in ("m", "M") and "[" not in dtype.str: # For datetime and timedelta dtypes, we have a tricky situation - # because they *may or may not* specify a unit as part of the dtype. # If not, we flatmap over the various resolutions so that array # elements have consistent units but units may vary between arrays. return ( st.sampled_from(TIME_RESOLUTIONS) .map((dtype.str + "[{}]").format) .flatmap(lambda d: arrays(d, shape=shape, fill=fill, unique=unique)) ) elements = from_dtype(dtype, **(elements or {})) check_strategy(elements, "elements") # If there's a redundant cast to the requested dtype, remove it. This unlocks # optimizations such as fast unique sampled_from, and saves some time directly too. unwrapped = unwrap_strategies(elements) if isinstance(unwrapped, MappedStrategy) and unwrapped.pack == dtype.type: elements = unwrapped.mapped_strategy if getattr(unwrapped, "force_has_reusable_values", False): elements.force_has_reusable_values = True # type: ignore if isinstance(shape, int): shape = (shape,) shape = tuple(shape) check_argument( all(isinstance(s, int) for s in shape), "Array shape must be integer in each dimension, provided shape was {}", shape, ) fill = fill_for(elements=elements, unique=unique, fill=fill) return ArrayStrategy(elements, shape, dtype, fill, unique) @defines_strategy() def scalar_dtypes() -> st.SearchStrategy[np.dtype]: """Return a strategy that can return any non-flexible scalar dtype.""" return st.one_of( boolean_dtypes(), integer_dtypes(), unsigned_integer_dtypes(), floating_dtypes(), complex_number_dtypes(), datetime64_dtypes(), timedelta64_dtypes(), ) def defines_dtype_strategy(strat: T) -> T: @defines_strategy() @proxies(strat) def inner(*args, **kwargs): return strat(*args, **kwargs).map(np.dtype) return inner @defines_dtype_strategy def boolean_dtypes() -> st.SearchStrategy["np.dtype[np.bool_]"]: """Return a strategy for boolean dtypes.""" return st.just("?") # type: ignore[arg-type] def dtype_factory(kind, sizes, valid_sizes, endianness): # Utility function, shared logic for most integer and string types valid_endian = ("?", "<", "=", ">") check_argument( endianness in valid_endian, "Unknown endianness: was {}, must be in {}", endianness, valid_endian, ) if valid_sizes is not None: if isinstance(sizes, int): sizes = (sizes,) check_argument(sizes, "Dtype must have at least one possible size.") check_argument( all(s in valid_sizes for s in sizes), "Invalid sizes: was {} must be an item or sequence in {}", sizes, valid_sizes, ) if all(isinstance(s, int) for s in sizes): sizes = sorted({s // 8 for s in sizes}) strat = st.sampled_from(sizes) if "{}" not in kind: kind += "{}" if endianness == "?": return strat.map(("<" + kind).format) | strat.map((">" + kind).format) return strat.map((endianness + kind).format) @overload def unsigned_integer_dtypes( *, endianness: str = "?", sizes: Literal[8], ) -> st.SearchStrategy["np.dtype[np.uint8]"]: ... @overload def unsigned_integer_dtypes( *, endianness: str = "?", sizes: Literal[16], ) -> st.SearchStrategy["np.dtype[np.uint16]"]: ... @overload def unsigned_integer_dtypes( *, endianness: str = "?", sizes: Literal[32], ) -> st.SearchStrategy["np.dtype[np.uint32]"]: ... @overload def unsigned_integer_dtypes( *, endianness: str = "?", sizes: Literal[64], ) -> st.SearchStrategy["np.dtype[np.uint64]"]: ... @overload def unsigned_integer_dtypes( *, endianness: str = "?", sizes: Sequence[Literal[8, 16, 32, 64]] = (8, 16, 32, 64), ) -> st.SearchStrategy["np.dtype[np.unsignedinteger[Any]]"]: ... @defines_dtype_strategy def unsigned_integer_dtypes( *, endianness: str = "?", sizes: Literal[8, 16, 32, 64] | Sequence[Literal[8, 16, 32, 64]] = ( 8, 16, 32, 64, ), ) -> st.SearchStrategy["np.dtype[np.unsignedinteger[Any]]"]: """Return a strategy for unsigned integer dtypes. endianness may be ``<`` for little-endian, ``>`` for big-endian, ``=`` for native byte order, or ``?`` to allow either byte order. This argument only applies to dtypes of more than one byte. sizes must be a collection of integer sizes in bits. The default (8, 16, 32, 64) covers the full range of sizes. """ return dtype_factory("u", sizes, (8, 16, 32, 64), endianness) @overload def integer_dtypes( *, endianness: str = "?", sizes: Literal[8], ) -> st.SearchStrategy["np.dtype[np.int8]"]: ... @overload def integer_dtypes( *, endianness: str = "?", sizes: Literal[16], ) -> st.SearchStrategy["np.dtype[np.int16]"]: ... @overload def integer_dtypes( *, endianness: str = "?", sizes: Literal[32], ) -> st.SearchStrategy["np.dtype[np.int32]"]: ... @overload def integer_dtypes( *, endianness: str = "?", sizes: Literal[64], ) -> st.SearchStrategy["np.dtype[np.int64]"]: ... @overload def integer_dtypes( *, endianness: str = "?", sizes: Sequence[Literal[8, 16, 32, 64]] = (8, 16, 32, 64), ) -> st.SearchStrategy["np.dtype[np.signedinteger[Any]]"]: ... @defines_dtype_strategy def integer_dtypes( *, endianness: str = "?", sizes: Literal[8, 16, 32, 64] | Sequence[Literal[8, 16, 32, 64]] = ( 8, 16, 32, 64, ), ) -> st.SearchStrategy["np.dtype[np.signedinteger[Any]]"]: """Return a strategy for signed integer dtypes. endianness and sizes are treated as for :func:`unsigned_integer_dtypes`. """ return dtype_factory("i", sizes, (8, 16, 32, 64), endianness) @overload def floating_dtypes( *, endianness: str = "?", sizes: Literal[16], ) -> st.SearchStrategy["np.dtype[np.float16]"]: ... @overload def floating_dtypes( *, endianness: str = "?", sizes: Literal[32], ) -> st.SearchStrategy["np.dtype[np.float32]"]: ... @overload def floating_dtypes( *, endianness: str = "?", sizes: Literal[64], ) -> st.SearchStrategy["np.dtype[np.float64]"]: ... @overload def floating_dtypes( *, endianness: str = "?", sizes: Literal[128], ) -> st.SearchStrategy["np.dtype[np.float128]"]: ... @overload def floating_dtypes( *, endianness: str = "?", sizes: Sequence[Literal[16, 32, 64, 96, 128]] = (16, 32, 64), ) -> st.SearchStrategy["np.dtype[np.floating[Any]]"]: ... @defines_dtype_strategy def floating_dtypes( *, endianness: str = "?", sizes: Literal[16, 32, 64, 96, 128] | Sequence[Literal[16, 32, 64, 96, 128]] = ( 16, 32, 64, ), ) -> st.SearchStrategy["np.dtype[np.floating[Any]]"]: """Return a strategy for floating-point dtypes. sizes is the size in bits of floating-point number. Some machines support 96- or 128-bit floats, but these are not generated by default. Larger floats (96 and 128 bit real parts) are not supported on all platforms and therefore disabled by default. To generate these dtypes, include these values in the sizes argument. """ return dtype_factory("f", sizes, (16, 32, 64, 96, 128), endianness) @overload def complex_number_dtypes( *, endianness: str = "?", sizes: Literal[64], ) -> st.SearchStrategy["np.dtype[np.complex64]"]: ... @overload def complex_number_dtypes( *, endianness: str = "?", sizes: Literal[128], ) -> st.SearchStrategy["np.dtype[np.complex128]"]: ... @overload def complex_number_dtypes( *, endianness: str = "?", sizes: Literal[256], ) -> st.SearchStrategy["np.dtype[np.complex256]"]: ... @overload def complex_number_dtypes( *, endianness: str = "?", sizes: Sequence[Literal[64, 128, 192, 256]] = (64, 128), ) -> st.SearchStrategy["np.dtype[np.complexfloating[Any, Any]]"]: ... @defines_dtype_strategy def complex_number_dtypes( *, endianness: str = "?", sizes: Literal[64, 128, 192, 256] | Sequence[Literal[64, 128, 192, 256]] = ( 64, 128, ), ) -> st.SearchStrategy["np.dtype[np.complexfloating[Any, Any]]"]: """Return a strategy for complex-number dtypes. sizes is the total size in bits of a complex number, which consists of two floats. Complex halves (a 16-bit real part) are not supported by numpy and will not be generated by this strategy. """ return dtype_factory("c", sizes, (64, 128, 192, 256), endianness) @check_function def validate_time_slice(max_period, min_period): check_argument( max_period in TIME_RESOLUTIONS, "max_period {} must be a valid resolution in {}", max_period, TIME_RESOLUTIONS, ) check_argument( min_period in TIME_RESOLUTIONS, "min_period {} must be a valid resolution in {}", min_period, TIME_RESOLUTIONS, ) start = TIME_RESOLUTIONS.index(max_period) end = TIME_RESOLUTIONS.index(min_period) + 1 check_argument( start < end, "max_period {} must be earlier in sequence {} than min_period {}", max_period, TIME_RESOLUTIONS, min_period, ) return TIME_RESOLUTIONS[start:end] @defines_dtype_strategy def datetime64_dtypes( *, max_period: str = "Y", min_period: str = "ns", endianness: str = "?" ) -> st.SearchStrategy["np.dtype[np.datetime64]"]: """Return a strategy for datetime64 dtypes, with various precisions from year to attosecond.""" return dtype_factory( "datetime64[{}]", validate_time_slice(max_period, min_period), TIME_RESOLUTIONS, endianness, ) @defines_dtype_strategy def timedelta64_dtypes( *, max_period: str = "Y", min_period: str = "ns", endianness: str = "?" ) -> st.SearchStrategy["np.dtype[np.timedelta64]"]: """Return a strategy for timedelta64 dtypes, with various precisions from year to attosecond.""" return dtype_factory( "timedelta64[{}]", validate_time_slice(max_period, min_period), TIME_RESOLUTIONS, endianness, ) @defines_dtype_strategy def byte_string_dtypes( *, endianness: str = "?", min_len: int = 1, max_len: int = 16 ) -> st.SearchStrategy["np.dtype[np.bytes_]"]: """Return a strategy for generating bytestring dtypes, of various lengths and byteorder. While Hypothesis' string strategies can generate empty strings, string dtypes with length 0 indicate that size is still to be determined, so the minimum length for string dtypes is 1. """ order_check("len", 1, min_len, max_len) return dtype_factory("S", list(range(min_len, max_len + 1)), None, endianness) @defines_dtype_strategy def unicode_string_dtypes( *, endianness: str = "?", min_len: int = 1, max_len: int = 16 ) -> st.SearchStrategy["np.dtype[np.str_]"]: """Return a strategy for generating unicode string dtypes, of various lengths and byteorder. While Hypothesis' string strategies can generate empty strings, string dtypes with length 0 indicate that size is still to be determined, so the minimum length for string dtypes is 1. """ order_check("len", 1, min_len, max_len) return dtype_factory("U", list(range(min_len, max_len + 1)), None, endianness) def _no_title_is_name_of_a_titled_field(ls): seen = set() for title_and_name, *_ in ls: if isinstance(title_and_name, tuple): if seen.intersection(title_and_name): # pragma: no cover # Our per-element filters below make this as rare as possible, # so it's not always covered. return False seen.update(title_and_name) return True @defines_dtype_strategy def array_dtypes( subtype_strategy: st.SearchStrategy[np.dtype] = scalar_dtypes(), *, min_size: int = 1, max_size: int = 5, allow_subarrays: bool = False, ) -> st.SearchStrategy[np.dtype]: """Return a strategy for generating array (compound) dtypes, with members drawn from the given subtype strategy.""" order_check("size", 0, min_size, max_size) # The empty string is replaced by f{idx}; see #1963 for details. Much easier to # insist that field names be unique and just boost f{idx} strings manually. field_names = st.integers(0, 127).map("f{}".format) | st.text(min_size=1) name_titles = st.one_of( field_names, st.tuples(field_names, field_names).filter(lambda ns: ns[0] != ns[1]), ) elements: st.SearchStrategy[tuple] = st.tuples(name_titles, subtype_strategy) if allow_subarrays: elements |= st.tuples( name_titles, subtype_strategy, array_shapes(max_dims=2, max_side=2) ) return st.lists( # type: ignore[return-value] elements=elements, min_size=min_size, max_size=max_size, unique_by=( # Deduplicate by both name and title for efficiency before filtering. # (Field names must be unique, as must titles, and no intersections) lambda d: d[0] if isinstance(d[0], str) else d[0][0], lambda d: d[0] if isinstance(d[0], str) else d[0][1], ), ).filter(_no_title_is_name_of_a_titled_field) @defines_strategy() def nested_dtypes( subtype_strategy: st.SearchStrategy[np.dtype] = scalar_dtypes(), *, max_leaves: int = 10, max_itemsize: int | None = None, ) -> st.SearchStrategy[np.dtype]: """Return the most-general dtype strategy. Elements drawn from this strategy may be simple (from the subtype_strategy), or several such values drawn from :func:`array_dtypes` with ``allow_subarrays=True``. Subdtypes in an array dtype may be nested to any depth, subject to the max_leaves argument. """ return st.recursive( subtype_strategy, lambda x: array_dtypes(x, allow_subarrays=True), max_leaves=max_leaves, ).filter(lambda d: max_itemsize is None or d.itemsize <= max_itemsize) @proxies(_valid_tuple_axes) def valid_tuple_axes(*args, **kwargs): return _valid_tuple_axes(*args, **kwargs) valid_tuple_axes.__doc__ = f""" Return a strategy for generating permissible tuple-values for the ``axis`` argument for a numpy sequential function (e.g. :func:`numpy:numpy.sum`), given an array of the specified dimensionality. {_valid_tuple_axes.__doc__} """ @proxies(_mutually_broadcastable_shapes) def mutually_broadcastable_shapes(*args, **kwargs): return _mutually_broadcastable_shapes(*args, **kwargs) mutually_broadcastable_shapes.__doc__ = f""" {_mutually_broadcastable_shapes.__doc__} **Use with Generalised Universal Function signatures** A :doc:`universal function ` (or ufunc for short) is a function that operates on ndarrays in an element-by-element fashion, supporting array broadcasting, type casting, and several other standard features. A :doc:`generalised ufunc ` operates on sub-arrays rather than elements, based on the "signature" of the function. Compare e.g. :obj:`numpy.add() ` (ufunc) to :obj:`numpy.matmul() ` (gufunc). To generate shapes for a gufunc, you can pass the ``signature`` argument instead of ``num_shapes``. This must be a gufunc signature string; which you can write by hand or access as e.g. ``np.matmul.signature`` on generalised ufuncs. In this case, the ``side`` arguments are applied to the 'core dimensions' as well, ignoring any frozen dimensions. ``base_shape`` and the ``dims`` arguments are applied to the 'loop dimensions', and if necessary, the dimensionality of each shape is silently capped to respect the 32-dimension limit. The generated ``result_shape`` is the real result shape of applying the gufunc to arrays of the generated ``input_shapes``, even where this is different to broadcasting the loop dimensions. gufunc-compatible shapes shrink their loop dimensions as above, towards omitting optional core dimensions, and smaller-size core dimensions. .. code-block:: pycon >>> # np.matmul.signature == "(m?,n),(n,p?)->(m?,p?)" >>> for _ in range(3): ... mutually_broadcastable_shapes(signature=np.matmul.signature).example() BroadcastableShapes(input_shapes=((2,), (2,)), result_shape=()) BroadcastableShapes(input_shapes=((3, 4, 2), (1, 2)), result_shape=(3, 4)) BroadcastableShapes(input_shapes=((4, 2), (1, 2, 3)), result_shape=(4, 3)) """ @overload def basic_indices( shape: Shape, *, min_dims: int = 0, max_dims: int | None = None, allow_newaxis: Literal[False] = ..., allow_ellipsis: Literal[False], ) -> st.SearchStrategy[ _BIENoEllipsisNoNewaxis | tuple[_BIENoEllipsisNoNewaxis, ...] ]: ... @overload def basic_indices( shape: Shape, *, min_dims: int = 0, max_dims: int | None = None, allow_newaxis: Literal[False] = ..., allow_ellipsis: Literal[True] = ..., ) -> st.SearchStrategy[_BIENoNewaxis | tuple[_BIENoNewaxis, ...]]: ... @overload def basic_indices( shape: Shape, *, min_dims: int = 0, max_dims: int | None = None, allow_newaxis: Literal[True], allow_ellipsis: Literal[False], ) -> st.SearchStrategy[_BIENoEllipsis | tuple[_BIENoEllipsis, ...]]: ... @overload def basic_indices( shape: Shape, *, min_dims: int = 0, max_dims: int | None = None, allow_newaxis: Literal[True], allow_ellipsis: Literal[True] = ..., ) -> st.SearchStrategy[_BIE | tuple[_BIE, ...]]: ... @defines_strategy() def basic_indices( shape: Shape, *, min_dims: int = 0, max_dims: int | None = None, allow_newaxis: bool = False, allow_ellipsis: bool = True, ) -> st.SearchStrategy[BasicIndex]: """Return a strategy for :doc:`basic indexes ` of arrays with the specified shape, which may include dimensions of size zero. It generates tuples containing some mix of integers, :obj:`python:slice` objects, ``...`` (an ``Ellipsis``), and ``None``. When a length-one tuple would be generated, this strategy may instead return the element which will index the first axis, e.g. ``5`` instead of ``(5,)``. * ``shape`` is the shape of the array that will be indexed, as a tuple of positive integers. This must be at least two-dimensional for a tuple to be a valid index; for one-dimensional arrays use :func:`~hypothesis.strategies.slices` instead. * ``min_dims`` is the minimum dimensionality of the resulting array from use of the generated index. When ``min_dims == 0``, scalars and zero-dimensional arrays are both allowed. * ``max_dims`` is the maximum dimensionality of the resulting array, defaulting to ``len(shape) if not allow_newaxis else max(len(shape), min_dims) + 2``. * ``allow_newaxis`` specifies whether ``None`` is allowed in the index. * ``allow_ellipsis`` specifies whether ``...`` is allowed in the index. """ # Arguments to exclude scalars, zero-dim arrays, and dims of size zero were # all considered and rejected. We want users to explicitly consider those # cases if they're dealing in general indexers, and while it's fiddly we can # back-compatibly add them later (hence using kwonlyargs). check_type(tuple, shape, "shape") check_argument( all(isinstance(x, int) and x >= 0 for x in shape), f"{shape=}, but all dimensions must be non-negative integers.", ) check_type(bool, allow_ellipsis, "allow_ellipsis") check_type(bool, allow_newaxis, "allow_newaxis") check_type(int, min_dims, "min_dims") if min_dims > len(shape) and not allow_newaxis: note_deprecation( f"min_dims={min_dims} is larger than len(shape)={len(shape)}, " "but allow_newaxis=False makes it impossible for an indexing " "operation to add dimensions.", since="2021-09-15", has_codemod=False, ) check_valid_dims(min_dims, "min_dims") if max_dims is None: if allow_newaxis: max_dims = min(max(len(shape), min_dims) + 2, NDIM_MAX) else: max_dims = min(len(shape), NDIM_MAX) else: check_type(int, max_dims, "max_dims") if max_dims > len(shape) and not allow_newaxis: note_deprecation( f"max_dims={max_dims} is larger than len(shape)={len(shape)}, " "but allow_newaxis=False makes it impossible for an indexing " "operation to add dimensions.", since="2021-09-15", has_codemod=False, ) check_valid_dims(max_dims, "max_dims") order_check("dims", 0, min_dims, max_dims) return BasicIndexStrategy( shape, min_dims=min_dims, max_dims=max_dims, allow_ellipsis=allow_ellipsis, allow_newaxis=allow_newaxis, allow_fewer_indices_than_dims=True, ) I = TypeVar("I", bound=np.integer) @overload def integer_array_indices( shape: Shape, *, result_shape: st.SearchStrategy[Shape] = array_shapes(), ) -> "st.SearchStrategy[tuple[NDArray[np.signedinteger[Any]], ...]]": ... @overload def integer_array_indices( shape: Shape, *, result_shape: st.SearchStrategy[Shape] = array_shapes(), dtype: "np.dtype[I]", ) -> "st.SearchStrategy[tuple[NDArray[I], ...]]": ... @defines_strategy() def integer_array_indices( shape: Shape, *, result_shape: st.SearchStrategy[Shape] = array_shapes(), dtype: "np.dtype[I] | np.dtype[np.signedinteger[Any] | np.bool[bool]]" = np.dtype( int ), ) -> "st.SearchStrategy[tuple[NDArray[I], ...]]": """Return a search strategy for tuples of integer-arrays that, when used to index into an array of shape ``shape``, given an array whose shape was drawn from ``result_shape``. Examples from this strategy shrink towards the tuple of index-arrays:: len(shape) * (np.zeros(drawn_result_shape, dtype), ) * ``shape`` a tuple of integers that indicates the shape of the array, whose indices are being generated. * ``result_shape`` a strategy for generating tuples of integers, which describe the shape of the resulting index arrays. The default is :func:`~hypothesis.extra.numpy.array_shapes`. The shape drawn from this strategy determines the shape of the array that will be produced when the corresponding example from ``integer_array_indices`` is used as an index. * ``dtype`` the integer data type of the generated index-arrays. Negative integer indices can be generated if a signed integer type is specified. Recall that an array can be indexed using a tuple of integer-arrays to access its members in an arbitrary order, producing an array with an arbitrary shape. For example: .. code-block:: pycon >>> from numpy import array >>> x = array([-0, -1, -2, -3, -4]) >>> ind = (array([[4, 0], [0, 1]]),) # a tuple containing a 2D integer-array >>> x[ind] # the resulting array is commensurate with the indexing array(s) array([[-4, 0], [0, -1]]) Note that this strategy does not accommodate all variations of so-called 'advanced indexing', as prescribed by NumPy's nomenclature. Combinations of basic and advanced indexes are too complex to usefully define in a standard strategy; we leave application-specific strategies to the user. Advanced-boolean indexing can be defined as ``arrays(shape=..., dtype=bool)``, and is similarly left to the user. """ check_type(tuple, shape, "shape") check_argument( shape and all(isinstance(x, int) and x > 0 for x in shape), f"{shape=} must be a non-empty tuple of integers > 0", ) check_strategy(result_shape, "result_shape") check_argument( np.issubdtype(dtype, np.integer), f"{dtype=} must be an integer dtype" ) signed = np.issubdtype(dtype, np.signedinteger) def array_for(index_shape, size): return arrays( dtype=dtype, shape=index_shape, elements=st.integers(-size if signed else 0, size - 1), ) return result_shape.flatmap( lambda index_shape: st.tuples(*(array_for(index_shape, size) for size in shape)) ) def _unpack_dtype(dtype): dtype_args = getattr(dtype, "__args__", ()) if dtype_args and type(dtype) not in (getattr(types, "UnionType", object()), Union): assert len(dtype_args) == 1 if isinstance(dtype_args[0], TypeVar): # numpy.dtype[+ScalarType] assert dtype_args[0].__bound__ == np.generic dtype = Any else: # plain dtype dtype = dtype_args[0] return dtype def _dtype_from_args(args): if len(args) <= 1: # Zero args: ndarray, _SupportsArray # One arg: ndarray[type], _SupportsArray[type] dtype = _unpack_dtype(args[0]) if args else Any else: # Two args: ndarray[shape, type], NDArray[*] assert len(args) == 2 dtype = _unpack_dtype(args[1]) if dtype is Any: return scalar_dtypes() elif type(dtype) in (getattr(types, "UnionType", object()), Union): return dtype return np.dtype(dtype) def _from_type(thing: type[Ex]) -> st.SearchStrategy[Ex] | None: """Called by st.from_type to try to infer a strategy for thing using numpy. If we can infer a numpy-specific strategy for thing, we return that; otherwise, we return None. """ base_strats = st.one_of( [ st.booleans(), st.integers(), st.floats(), st.complex_numbers(), st.text(), st.binary(), ] ) # don't mix strings and non-ascii bytestrings (ex: ['', b'\x80']). See # https://github.com/numpy/numpy/issues/23899. base_strats_ascii = st.one_of( [ st.booleans(), st.integers(), st.floats(), st.complex_numbers(), st.text(), st.binary().filter(bytes.isascii), ] ) if thing == np.dtype: # Note: Parameterized dtypes and DTypeLike are not supported. return st.one_of( scalar_dtypes(), byte_string_dtypes(), unicode_string_dtypes(), array_dtypes(), nested_dtypes(), ) if thing == ArrayLike: # We override the default type resolution to ensure the "coercible to # array" contract is honoured. See # https://github.com/HypothesisWorks/hypothesis/pull/3670#issuecomment-1578140422. # The actual type is (as of np 1.24), with # scalars:=[bool, int, float, complex, str, bytes]: # Union[ # _SupportsArray, # _NestedSequence[_SupportsArray], # *scalars, # _NestedSequence[Union[*scalars]] # ] return st.one_of( # *scalars base_strats, # The two recursive strategies below cover the following cases: # - _SupportsArray (using plain ndarrays) # - _NestedSequence[Union[*scalars]] (but excluding non-ascii binary) # - _NestedSequence[_SupportsArray] (but with a single leaf element # . to avoid the issue of unequally sized leaves) st.recursive(st.lists(base_strats_ascii), extend=st.tuples), st.recursive(st.from_type(np.ndarray), extend=st.tuples), ) if isinstance(thing, type) and issubclass(thing, np.generic): dtype = np.dtype(thing) return from_dtype(dtype) if dtype.kind not in "OV" else None origin = get_origin(thing) # if origin is not generic-like, get_origin returns None. Fall back to thing. if origin is None: origin = thing args = get_args(thing) if origin == _NestedSequence: # We have to override the default resolution to ensure sequences are of # equal length. Actually they are still not, if the arg specialization # returns arbitrary-shaped sequences or arrays - hence the even more special # resolution of ArrayLike, above. assert len(args) <= 1 base_strat = st.from_type(args[0]) if args else base_strats return st.one_of( st.lists(base_strat), st.recursive(st.tuples(), st.tuples), st.recursive(st.tuples(base_strat), st.tuples), st.recursive(st.tuples(base_strat, base_strat), st.tuples), ) # note: get_origin(np.typing.NDArray[np.int64]) is np.ndarray in numpy < 2.5.0, # but is np.typing.NDArray in numpy >= 2.5.0. Support both here. if origin in [np.typing.NDArray, np.ndarray, _SupportsArray]: dtype = _dtype_from_args(args) return arrays(dtype, array_shapes(max_dims=2)) # type: ignore[return-value] # We didn't find a type to resolve, continue return None ================================================ FILE: hypothesis-python/src/hypothesis/extra/pandas/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.extra.pandas.impl import ( column, columns, data_frames, indexes, range_indexes, series, ) __all__ = ["column", "columns", "data_frames", "indexes", "range_indexes", "series"] ================================================ FILE: hypothesis-python/src/hypothesis/extra/pandas/impl.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import OrderedDict, abc from collections.abc import Sequence from copy import copy from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, Generic, Union import numpy as np import pandas from hypothesis import strategies as st from hypothesis.control import reject from hypothesis.errors import InvalidArgument from hypothesis.extra import numpy as npst from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.coverage import check, check_function from hypothesis.internal.validation import ( check_type, check_valid_interval, check_valid_size, try_convert, ) from hypothesis.strategies._internal.strategies import Ex, check_strategy from hypothesis.strategies._internal.utils import cacheable, defines_strategy from hypothesis.utils.deprecation import note_deprecation try: from pandas.core.arrays.integer import IntegerDtype except ImportError: IntegerDtype = () def dtype_for_elements_strategy(s): return st.shared( s.map(lambda x: pandas.Series([x]).dtype), key=("hypothesis.extra.pandas.dtype_for_elements_strategy", s), ) def infer_dtype_if_necessary(dtype, values, elements, draw): if dtype is None and not values: return draw(dtype_for_elements_strategy(elements)) return dtype @check_function def elements_and_dtype(elements, dtype, source=None): if source is None: prefix = "" else: prefix = f"{source}." if elements is not None: check_strategy(elements, f"{prefix}elements") else: with check("dtype is not None"): if dtype is None: raise InvalidArgument( f"At least one of {prefix}elements or {prefix}dtype must be provided." ) with check("isinstance(dtype, CategoricalDtype)"): if pandas.api.types.CategoricalDtype.is_dtype(dtype): raise InvalidArgument( f"{prefix}dtype is categorical, which is currently unsupported" ) if isinstance(dtype, type) and issubclass(dtype, IntegerDtype): raise InvalidArgument( f"Passed {dtype=} is a dtype class, please pass in an instance of this class." "Otherwise it would be treated as dtype=object" ) if isinstance(dtype, type) and np.dtype(dtype).kind == "O" and dtype is not object: err_msg = f"Passed {dtype=} is not a valid Pandas dtype." if issubclass(dtype, datetime): err_msg += ' To generate valid datetimes, pass `dtype="datetime64[ns]"`' raise InvalidArgument(err_msg) elif issubclass(dtype, timedelta): err_msg += ' To generate valid timedeltas, pass `dtype="timedelta64[ns]"`' raise InvalidArgument(err_msg) note_deprecation( f"{err_msg} We'll treat it as " "dtype=object for now, but this will be an error in a future version.", since="2021-12-31", has_codemod=False, stacklevel=1, ) if isinstance(dtype, st.SearchStrategy): raise InvalidArgument( f"Passed {dtype=} is a strategy, but we require a concrete dtype " "here. See https://stackoverflow.com/q/74355937 for workaround patterns." ) _get_subclasses = getattr(IntegerDtype, "__subclasses__", list) dtype = {t.name: t() for t in _get_subclasses()}.get(dtype, dtype) is_na_dtype = False if isinstance(dtype, IntegerDtype): is_na_dtype = True dtype = np.dtype(dtype.name.lower()) elif dtype is not None: dtype = try_convert(np.dtype, dtype, "dtype") if elements is None: elements = npst.from_dtype(dtype) if is_na_dtype: elements = st.none() | elements # as an optimization, avoid converting object dtypes, which will always # remain unchanged. elif dtype is not None and dtype.kind != "O": def convert_element(value): if is_na_dtype and value is None: return None try: return np.array([value], dtype=dtype)[0] except (TypeError, ValueError, OverflowError): name = f"draw({prefix}elements)" raise InvalidArgument( f"Cannot convert {name}={value!r} of type " f"{type(value).__name__} to dtype {dtype.str}" ) from None elements = elements.map(convert_element) assert elements is not None return elements, dtype class ValueIndexStrategy(st.SearchStrategy): def __init__(self, elements, dtype, min_size, max_size, unique, name): super().__init__() self.elements = elements self.dtype = dtype self.min_size = min_size self.max_size = max_size self.unique = unique self.name = name def do_draw(self, data): result = [] seen = set() iterator = cu.many( data, min_size=self.min_size, max_size=self.max_size, average_size=(self.min_size + self.max_size) / 2, ) while iterator.more(): elt = data.draw(self.elements) if self.unique: if elt in seen: iterator.reject() continue seen.add(elt) result.append(elt) dtype = infer_dtype_if_necessary( dtype=self.dtype, values=result, elements=self.elements, draw=data.draw ) return pandas.Index( result, dtype=dtype, tupleize_cols=False, name=data.draw(self.name) ) DEFAULT_MAX_SIZE = 10 @cacheable @defines_strategy() def range_indexes( min_size: int = 0, max_size: int | None = None, name: st.SearchStrategy[str | None] = st.none(), ) -> st.SearchStrategy[pandas.RangeIndex]: """Provides a strategy which generates an :class:`~pandas.Index` whose values are 0, 1, ..., n for some n. Arguments: * min_size is the smallest number of elements the index can have. * max_size is the largest number of elements the index can have. If None it will default to some suitable value based on min_size. * name is the name of the index. If st.none(), the index will have no name. """ check_valid_size(min_size, "min_size") check_valid_size(max_size, "max_size") if max_size is None: max_size = min([min_size + DEFAULT_MAX_SIZE, 2**63 - 1]) check_valid_interval(min_size, max_size, "min_size", "max_size") check_strategy(name) return st.builds(pandas.RangeIndex, st.integers(min_size, max_size), name=name) @cacheable @defines_strategy() def indexes( *, elements: st.SearchStrategy[Ex] | None = None, dtype: Any = None, min_size: int = 0, max_size: int | None = None, unique: bool = True, name: st.SearchStrategy[str | None] = st.none(), ) -> st.SearchStrategy[pandas.Index]: """Provides a strategy for producing a :class:`pandas.Index`. Arguments: * elements is a strategy which will be used to generate the individual values of the index. If None, it will be inferred from the dtype. Note: even if the elements strategy produces tuples, the generated value will not be a MultiIndex, but instead be a normal index whose elements are tuples. * dtype is the dtype of the resulting index. If None, it will be inferred from the elements strategy. At least one of dtype or elements must be provided. * min_size is the minimum number of elements in the index. * max_size is the maximum number of elements in the index. If None then it will default to a suitable small size. If you want larger indexes you should pass a max_size explicitly. * unique specifies whether all of the elements in the resulting index should be distinct. * name is a strategy for strings or ``None``, which will be passed to the :class:`pandas.Index` constructor. """ check_valid_size(min_size, "min_size") check_valid_size(max_size, "max_size") check_valid_interval(min_size, max_size, "min_size", "max_size") check_type(bool, unique, "unique") elements, dtype = elements_and_dtype(elements, dtype) if max_size is None: max_size = min_size + DEFAULT_MAX_SIZE return ValueIndexStrategy(elements, dtype, min_size, max_size, unique, name) @defines_strategy() def series( *, elements: st.SearchStrategy[Ex] | None = None, dtype: Any = None, # new-style unions hit https://github.com/sphinx-doc/sphinx/issues/11211 during # doc builds. See related comment in django/_fields.py. Quote to prevent # shed/pyupgrade from changing it. index: ( st.SearchStrategy["Union[Sequence, pandas.Index]"] | None # noqa: UP007 ) = None, fill: st.SearchStrategy[Ex] | None = None, unique: bool = False, name: st.SearchStrategy[str | None] = st.none(), ) -> st.SearchStrategy[pandas.Series]: """Provides a strategy for producing a :class:`pandas.Series`. Arguments: * elements: a strategy that will be used to generate the individual values in the series. If None, we will attempt to infer a suitable default from the dtype. * dtype: the dtype of the resulting series and may be any value that can be passed to :class:`numpy.dtype`. If None, will use pandas's standard behaviour to infer it from the type of the elements values. Note that if the type of values that comes out of your elements strategy varies, then so will the resulting dtype of the series. * index: If not None, a strategy for generating indexes for the resulting Series. This can generate either :class:`pandas.Index` objects or any sequence of values (which will be passed to the Index constructor). You will probably find it most convenient to use the :func:`~hypothesis.extra.pandas.indexes` or :func:`~hypothesis.extra.pandas.range_indexes` function to produce values for this argument. * name: is a strategy for strings or ``None``, which will be passed to the :class:`pandas.Series` constructor. Usage: .. code-block:: pycon >>> series(dtype=int).example() 0 -2001747478 1 1153062837 """ if index is None: index = range_indexes() else: check_strategy(index, "index") elements, np_dtype = elements_and_dtype(elements, dtype) index_strategy = index # if it is converted to an object, use object for series type if ( np_dtype is not None and np_dtype.kind == "O" and not isinstance(dtype, IntegerDtype) ): dtype = np_dtype @st.composite def result(draw): index = draw(index_strategy) if len(index) > 0: if dtype is not None: result_data = draw( npst.arrays( dtype=object, elements=elements, shape=len(index), fill=fill, unique=unique, ) ).tolist() else: result_data = list( draw( npst.arrays( dtype=object, elements=elements, shape=len(index), fill=fill, unique=unique, ) ).tolist() ) return pandas.Series(result_data, index=index, dtype=dtype, name=draw(name)) else: return pandas.Series( (), index=index, dtype=( dtype if dtype is not None else draw(dtype_for_elements_strategy(elements)) ), name=draw(name), ) return result() @dataclass(slots=True, frozen=False) class column(Generic[Ex]): """Data object for describing a column in a DataFrame. Arguments: * name: the column name, or None to default to the column position. Must be hashable, but can otherwise be any value supported as a pandas column name. * elements: the strategy for generating values in this column, or None to infer it from the dtype. * dtype: the dtype of the column, or None to infer it from the element strategy. At least one of dtype or elements must be provided. * fill: A default value for elements of the column. See :func:`~hypothesis.extra.numpy.arrays` for a full explanation. * unique: If all values in this column should be distinct. """ name: str | int | None = None elements: st.SearchStrategy[Ex] | None = None dtype: Any = None fill: st.SearchStrategy[Ex] | None = None unique: bool = False def columns( names_or_number: int | Sequence[str], *, dtype: Any = None, elements: st.SearchStrategy[Ex] | None = None, fill: st.SearchStrategy[Ex] | None = None, unique: bool = False, ) -> list[column[Ex]]: """A convenience function for producing a list of :class:`column` objects of the same general shape. The names_or_number argument is either a sequence of values, the elements of which will be used as the name for individual column objects, or a number, in which case that many unnamed columns will be created. All other arguments are passed through verbatim to create the columns. """ if isinstance(names_or_number, (int, float)): names: list[int | str | None] = [None] * names_or_number else: names = list(names_or_number) return [ column(name=n, dtype=dtype, elements=elements, fill=fill, unique=unique) for n in names ] @defines_strategy() def data_frames( columns: Sequence[column] | None = None, *, rows: st.SearchStrategy[dict | Sequence[Any]] | None = None, index: st.SearchStrategy[Ex] | None = None, ) -> st.SearchStrategy[pandas.DataFrame]: """Provides a strategy for producing a :class:`pandas.DataFrame`. Arguments: * columns: An iterable of :class:`column` objects describing the shape of the generated DataFrame. * rows: A strategy for generating a row object. Should generate either dicts mapping column names to values or a sequence mapping column position to the value in that position (note that unlike the :class:`pandas.DataFrame` constructor, single values are not allowed here. Passing e.g. an integer is an error, even if there is only one column). At least one of rows and columns must be provided. If both are provided then the generated rows will be validated against the columns and an error will be raised if they don't match. Caveats on using rows: * In general you should prefer using columns to rows, and only use rows if the columns interface is insufficiently flexible to describe what you need - you will get better performance and example quality that way. * If you provide rows and not columns, then the shape and dtype of the resulting DataFrame may vary. e.g. if you have a mix of int and float in the values for one column in your row entries, the column will sometimes have an integral dtype and sometimes a float. * index: If not None, a strategy for generating indexes for the resulting DataFrame. This can generate either :class:`pandas.Index` objects or any sequence of values (which will be passed to the Index constructor). You will probably find it most convenient to use the :func:`~hypothesis.extra.pandas.indexes` or :func:`~hypothesis.extra.pandas.range_indexes` function to produce values for this argument. Usage: The expected usage pattern is that you use :class:`column` and :func:`columns` to specify a fixed shape of the DataFrame you want as follows. For example the following gives a two column data frame: .. code-block:: pycon >>> from hypothesis.extra.pandas import column, data_frames >>> data_frames([ ... column('A', dtype=int), column('B', dtype=float)]).example() A B 0 2021915903 1.793898e+232 1 1146643993 inf 2 -2096165693 1.000000e+07 If you want the values in different columns to interact in some way you can use the rows argument. For example the following gives a two column DataFrame where the value in the first column is always at most the value in the second: .. code-block:: pycon >>> from hypothesis.extra.pandas import column, data_frames >>> import hypothesis.strategies as st >>> data_frames( ... rows=st.tuples(st.floats(allow_nan=False), ... st.floats(allow_nan=False)).map(sorted) ... ).example() 0 1 0 -3.402823e+38 9.007199e+15 1 -1.562796e-298 5.000000e-01 You can also combine the two: .. code-block:: pycon >>> from hypothesis.extra.pandas import columns, data_frames >>> import hypothesis.strategies as st >>> data_frames( ... columns=columns(["lo", "hi"], dtype=float), ... rows=st.tuples(st.floats(allow_nan=False), ... st.floats(allow_nan=False)).map(sorted) ... ).example() lo hi 0 9.314723e-49 4.353037e+45 1 -9.999900e-01 1.000000e+07 2 -2.152861e+134 -1.069317e-73 (Note that the column dtype must still be specified and will not be inferred from the rows. This restriction may be lifted in future). Combining rows and columns has the following behaviour: * The column names and dtypes will be used. * If the column is required to be unique, this will be enforced. * Any values missing from the generated rows will be provided using the column's fill. * Any values in the row not present in the column specification (if dicts are passed, if there are keys with no corresponding column name, if sequences are passed if there are too many items) will result in InvalidArgument being raised. """ if index is None: index = range_indexes() else: check_strategy(index, "index") index_strategy = index if columns is None: if rows is None: raise InvalidArgument("At least one of rows and columns must be provided") else: @st.composite def rows_only(draw): index = draw(index_strategy) def row(): result = draw(rows) check_type(abc.Iterable, result, "draw(row)") return result if len(index) > 0: return pandas.DataFrame([row() for _ in index], index=index) else: # If we haven't drawn any rows we need to draw one row and # then discard it so that we get a consistent shape for the # DataFrame. base = pandas.DataFrame([row()]) return base.drop(0) return rows_only() assert columns is not None cols = try_convert(tuple, columns, "columns") rewritten_columns = [] column_names: set[str] = set() for i, c in enumerate(cols): check_type(column, c, f"columns[{i}]") c = copy(c) if c.name is None: label = f"columns[{i}]" c.name = i else: label = c.name try: hash(c.name) except TypeError: raise InvalidArgument( f"Column names must be hashable, but columns[{i}].name was " f"{c.name!r} of type {type(c.name).__name__}, which cannot be hashed." ) from None if c.name in column_names: raise InvalidArgument(f"duplicate definition of column name {c.name!r}") column_names.add(c.name) c.elements, _ = elements_and_dtype(c.elements, c.dtype, label) if c.dtype is None and rows is not None: raise InvalidArgument( "Must specify a dtype for all columns when combining rows with columns." ) c.fill = npst.fill_for( fill=c.fill, elements=c.elements, unique=c.unique, name=label ) rewritten_columns.append(c) if rows is None: @st.composite def just_draw_columns(draw): index = draw(index_strategy) local_index_strategy = st.just(index) data = OrderedDict((c.name, None) for c in rewritten_columns) # Depending on how the columns are going to be generated we group # them differently to get better shrinking. For columns with fill # enabled, the elements can be shrunk independently of the size, # so we can just shrink by shrinking the index then shrinking the # length and are generally much more free to move data around. # For columns with no filling the problem is harder, and drawing # them like that would result in rows being very far apart from # each other in the choice sequence, which gets in the way # of shrinking. So what we do is reorder and draw those columns # row wise, so that the values of each row are next to each other. # This makes life easier for the shrinker when deleting choices. columns_without_fill = [c for c in rewritten_columns if c.fill.is_empty] if columns_without_fill: for c in columns_without_fill: data[c.name] = pandas.Series( np.zeros(shape=len(index), dtype=object), index=index, dtype=c.dtype, ) seen = {c.name: set() for c in columns_without_fill if c.unique} for i in range(len(index)): for c in columns_without_fill: if c.unique: for _ in range(5): value = draw(c.elements) if value not in seen[c.name]: seen[c.name].add(value) break else: reject() else: value = draw(c.elements) try: data[c.name].iloc[i] = value except ValueError as err: # pragma: no cover # This just works in Pandas 1.4 and later, but gives # a confusing error on previous versions. if c.dtype is None and not isinstance( value, (float, int, str, bool, datetime, timedelta) ): raise ValueError( f"Failed to add {value=} to column " f"{c.name} with dtype=None. Maybe passing " "dtype=object would help?" ) from err # Unclear how this could happen, but users find a way... raise for c in rewritten_columns: if not c.fill.is_empty: data[c.name] = draw( series( index=local_index_strategy, dtype=c.dtype, elements=c.elements, fill=c.fill, unique=c.unique, ) ) return pandas.DataFrame(data, index=index) return just_draw_columns() else: @st.composite def assign_rows(draw): index = draw(index_strategy) result = pandas.DataFrame( OrderedDict( ( c.name, pandas.Series( np.zeros(dtype=c.dtype, shape=len(index)), dtype=c.dtype ), ) for c in rewritten_columns ), index=index, ) fills = {} any_unique = any(c.unique for c in rewritten_columns) if any_unique: all_seen = [set() if c.unique else None for c in rewritten_columns] while all_seen[-1] is None: all_seen.pop() for row_index in range(len(index)): for _ in range(5): original_row = draw(rows) row = original_row if isinstance(row, dict): as_list = [None] * len(rewritten_columns) for i, c in enumerate(rewritten_columns): try: as_list[i] = row[c.name] except KeyError: try: as_list[i] = fills[i] except KeyError: if c.fill.is_empty: raise InvalidArgument( f"Empty fill strategy in {c!r} cannot " f"complete row {original_row!r}" ) from None fills[i] = draw(c.fill) as_list[i] = fills[i] for k in row: if k not in column_names: raise InvalidArgument( f"Row {row!r} contains column {k!r} not in " f"columns {[c.name for c in rewritten_columns]!r})" ) row = as_list if any_unique: has_duplicate = False for seen, value in zip(all_seen, row, strict=False): if seen is None: continue if value in seen: has_duplicate = True break seen.add(value) if has_duplicate: continue row = list(try_convert(tuple, row, "draw(rows)")) if len(row) > len(rewritten_columns): raise InvalidArgument( f"Row {original_row!r} contains too many entries. Has " f"{len(row)} but expected at most {len(rewritten_columns)}" ) while len(row) < len(rewritten_columns): c = rewritten_columns[len(row)] if c.fill.is_empty: raise InvalidArgument( f"Empty fill strategy in {c!r} cannot " f"complete row {original_row!r}" ) row.append(draw(c.fill)) result.iloc[row_index] = row break else: reject() return result return assign_rows() ================================================ FILE: hypothesis-python/src/hypothesis/extra/pytestplugin.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ Stub for users who manually load our pytest plugin. The plugin implementation is now located in a top-level module outside the main hypothesis tree, so that Pytest can load the plugin without thereby triggering the import of Hypothesis itself (and thus loading our own plugins). """ from _hypothesis_pytestplugin import * # noqa ================================================ FILE: hypothesis-python/src/hypothesis/extra/pytz.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ This module provides :pypi:`pytz` timezones. If you are unable to use the stdlib :mod:`zoneinfo` module, e.g. via the :func:`hypothesis.strategies.timezones` strategy, you can use this strategy with :py:func:`hypothesis.strategies.datetimes` and :py:func:`hypothesis.strategies.times` to produce timezone-aware values. .. warning:: Since :mod:`zoneinfo` was added in Python 3.9, this extra is deprecated. We intend to remove it after libraries such as Pandas and Django complete their own migrations. """ import datetime as dt import pytz from pytz.tzfile import StaticTzInfo # type: ignore # considered private by typeshed from hypothesis import strategies as st from hypothesis.strategies._internal.utils import cacheable, defines_strategy __all__ = ["timezones"] @cacheable @defines_strategy() def timezones() -> st.SearchStrategy[dt.tzinfo]: """Any timezone in the Olsen database, as a pytz tzinfo object. This strategy minimises to UTC, or the smallest possible fixed offset, and is designed for use with :func:`hypothesis.strategies.datetimes`. .. tip:: Prefer the :func:`hypothesis.strategies.timezones` strategy, which uses the stdlib :mod:`zoneinfo` module and avoids `the many footguns in pytz `__. """ all_timezones = [pytz.timezone(tz) for tz in pytz.all_timezones] # Some timezones have always had a constant offset from UTC. This makes # them simpler than timezones with daylight savings, and the smaller the # absolute offset the simpler they are. Of course, UTC is even simpler! static: list = [pytz.UTC] static += sorted( (t for t in all_timezones if isinstance(t, StaticTzInfo)), key=lambda tz: abs(tz.utcoffset(dt.datetime(2000, 1, 1))), ) # Timezones which have changed UTC offset; best ordered by name. dynamic = [tz for tz in all_timezones if tz not in static] return st.sampled_from(static + dynamic) ================================================ FILE: hypothesis-python/src/hypothesis/extra/redis.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import base64 import json from collections.abc import Iterable from contextlib import contextmanager from datetime import timedelta from typing import Any from redis import Redis from hypothesis.database import ExampleDatabase from hypothesis.internal.validation import check_type class RedisExampleDatabase(ExampleDatabase): """Store Hypothesis examples as sets in the given :class:`~redis.Redis` datastore. This is particularly useful for shared databases, as per the recipe for a :class:`~hypothesis.database.MultiplexedDatabase`. .. note:: If a test has not been run for ``expire_after``, those examples will be allowed to expire. The default time-to-live persists examples between weekly runs. """ def __init__( self, redis: Redis, *, expire_after: timedelta = timedelta(days=8), key_prefix: bytes = b"hypothesis-example:", listener_channel: str = "hypothesis-changes", ): super().__init__() check_type(Redis, redis, "redis") check_type(timedelta, expire_after, "expire_after") check_type(bytes, key_prefix, "key_prefix") check_type(str, listener_channel, "listener_channel") self.redis = redis self._expire_after = expire_after self._prefix = key_prefix self.listener_channel = listener_channel self._pubsub: Any = None def __repr__(self) -> str: return ( f"RedisExampleDatabase({self.redis!r}, expire_after={self._expire_after!r})" ) def __eq__(self, other: object) -> bool: return ( isinstance(other, RedisExampleDatabase) and self.redis == other.redis and self._prefix == other._prefix and self.listener_channel == other.listener_channel ) @contextmanager def _pipeline( self, *reset_expire_keys, execute_and_publish=True, event_type=None, to_publish=None, ): # Context manager to batch updates and expiry reset, reducing TCP roundtrips pipe = self.redis.pipeline() yield pipe for key in reset_expire_keys: pipe.expire(self._prefix + key, self._expire_after) if execute_and_publish: changed = pipe.execute() # pipe.execute returns the rows modified for each operation, which includes # the operations performed during the yield, followed by the n operations # from pipe.exire. Look at just the operations from during the yield. changed = changed[: -len(reset_expire_keys)] if any(count > 0 for count in changed): assert to_publish is not None assert event_type is not None self._publish((event_type, to_publish)) def _publish(self, event): event = (event[0], tuple(self._encode(v) for v in event[1])) self.redis.publish(self.listener_channel, json.dumps(event)) def _encode(self, value: bytes) -> str: return base64.b64encode(value).decode("ascii") def _decode(self, value: str) -> bytes: return base64.b64decode(value) def fetch(self, key: bytes) -> Iterable[bytes]: with self._pipeline(key, execute_and_publish=False) as pipe: pipe.smembers(self._prefix + key) yield from pipe.execute()[0] def save(self, key: bytes, value: bytes) -> None: with self._pipeline(key, event_type="save", to_publish=(key, value)) as pipe: pipe.sadd(self._prefix + key, value) def delete(self, key: bytes, value: bytes) -> None: with self._pipeline(key, event_type="delete", to_publish=(key, value)) as pipe: pipe.srem(self._prefix + key, value) def move(self, src: bytes, dest: bytes, value: bytes) -> None: if src == dest: self.save(dest, value) return with self._pipeline(src, dest, execute_and_publish=False) as pipe: pipe.srem(self._prefix + src, value) pipe.sadd(self._prefix + dest, value) changed = pipe.execute() if changed[0] > 0: # did the value set of the first key change? self._publish(("delete", (src, value))) if changed[1] > 0: # did the value set of the second key change? self._publish(("save", (dest, value))) def _handle_message(self, message: dict) -> None: # other message types include "subscribe" and "unsubscribe". these are # sent to the client, but not to the pubsub channel. assert message["type"] == "message" data = json.loads(message["data"]) event_type = data[0] self._broadcast_change( (event_type, tuple(self._decode(v) for v in data[1])) # type: ignore ) def _start_listening(self) -> None: self._pubsub = self.redis.pubsub() self._pubsub.subscribe(**{self.listener_channel: self._handle_message}) def _stop_listening(self) -> None: self._pubsub.unsubscribe() self._pubsub.close() self._pubsub = None ================================================ FILE: hypothesis-python/src/hypothesis/internal/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/src/hypothesis/internal/cache.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import threading from collections import OrderedDict from dataclasses import dataclass from typing import Any, Generic, TypeVar from hypothesis.errors import InvalidArgument K = TypeVar("K") V = TypeVar("V") @dataclass(slots=True, frozen=False) class Entry(Generic[K, V]): key: K value: V score: int pins: int = 0 @property def sort_key(self) -> tuple[int, ...]: if self.pins == 0: # Unpinned entries are sorted by score. return (0, self.score) else: # Pinned entries sort after unpinned ones. Beyond that, we don't # worry about their relative order. return (1,) class GenericCache(Generic[K, V]): """Generic supertype for cache implementations. Defines a dict-like mapping with a maximum size, where as well as mapping to a value, each key also maps to a score. When a write would cause the dict to exceed its maximum size, it first evicts the existing key with the smallest score, then adds the new key to the map. If due to pinning no key can be evicted, ValueError is raised. A key has the following lifecycle: 1. key is written for the first time, the key is given the score self.new_entry(key, value) 2. whenever an existing key is read or written, self.on_access(key, value, score) is called. This returns a new score for the key. 3. After a key is evicted, self.on_evict(key, value, score) is called. The cache will be in a valid state in all of these cases. Implementations are expected to implement new_entry and optionally on_access and on_evict to implement a specific scoring strategy. """ __slots__ = ("_threadlocal", "max_size") def __init__(self, max_size: int): if max_size <= 0: raise InvalidArgument("Cache size must be at least one.") self.max_size = max_size # Implementation: We store a binary heap of Entry objects in self.data, # with the heap property requiring that a parent's score is <= that of # its children. keys_to_index then maps keys to their index in the # heap. We keep these two in sync automatically - the heap is never # reordered without updating the index. self._threadlocal = threading.local() @property def keys_to_indices(self) -> dict[K, int]: try: return self._threadlocal.keys_to_indices except AttributeError: self._threadlocal.keys_to_indices = {} return self._threadlocal.keys_to_indices @property def data(self) -> list[Entry[K, V]]: try: return self._threadlocal.data except AttributeError: self._threadlocal.data = [] return self._threadlocal.data def __len__(self) -> int: assert len(self.keys_to_indices) == len(self.data) return len(self.data) def __contains__(self, key: K) -> bool: return key in self.keys_to_indices def __getitem__(self, key: K) -> V: i = self.keys_to_indices[key] result = self.data[i] self.__entry_was_accessed(i) return result.value def __setitem__(self, key: K, value: V) -> None: evicted = None try: i = self.keys_to_indices[key] except KeyError: entry = Entry(key, value, self.new_entry(key, value)) if len(self.data) >= self.max_size: evicted = self.data[0] if evicted.pins > 0: raise ValueError( "Cannot increase size of cache where all keys have been pinned." ) from None del self.keys_to_indices[evicted.key] i = 0 self.data[0] = entry else: i = len(self.data) self.data.append(entry) self.keys_to_indices[key] = i self.__balance(i) else: entry = self.data[i] assert entry.key == key entry.value = value self.__entry_was_accessed(i) if evicted is not None: if self.data[0] is not entry: assert evicted.sort_key <= self.data[0].sort_key self.on_evict(evicted.key, evicted.value, evicted.score) def __iter__(self): return iter(self.keys_to_indices) def pin(self, key: K, value: V) -> None: """Mark ``key`` as pinned (with the given value). That is, it may not be evicted until ``unpin(key)`` has been called. The same key may be pinned multiple times, possibly changing its value, and will not be unpinned until the same number of calls to unpin have been made. """ self[key] = value i = self.keys_to_indices[key] entry = self.data[i] entry.pins += 1 if entry.pins == 1: self.__balance(i) def unpin(self, key: K) -> None: """Undo one previous call to ``pin(key)``. The value stays the same. Once all calls are undone this key may be evicted as normal.""" i = self.keys_to_indices[key] entry = self.data[i] if entry.pins == 0: raise ValueError(f"Key {key!r} has not been pinned") entry.pins -= 1 if entry.pins == 0: self.__balance(i) def is_pinned(self, key: K) -> bool: """Returns True if the key is currently pinned.""" i = self.keys_to_indices[key] return self.data[i].pins > 0 def clear(self) -> None: """Remove all keys, regardless of their pinned status.""" del self.data[:] self.keys_to_indices.clear() def __repr__(self) -> str: return "{" + ", ".join(f"{e.key!r}: {e.value!r}" for e in self.data) + "}" def new_entry(self, key: K, value: V) -> int: """Called when a key is written that does not currently appear in the map. Returns the score to associate with the key. """ raise NotImplementedError def on_access(self, key: K, value: V, score: Any) -> Any: """Called every time a key that is already in the map is read or written. Returns the new score for the key. """ return score def on_evict(self, key: K, value: V, score: Any) -> Any: """Called after a key has been evicted, with the score it had had at the point of eviction.""" def check_valid(self) -> None: """Debugging method for use in tests. Asserts that all of the cache's invariants hold. When everything is working correctly this should be an expensive no-op. """ assert len(self.keys_to_indices) == len(self.data) for i, e in enumerate(self.data): assert self.keys_to_indices[e.key] == i for j in [i * 2 + 1, i * 2 + 2]: if j < len(self.data): assert e.sort_key <= self.data[j].sort_key, self.data def __entry_was_accessed(self, i: int) -> None: entry = self.data[i] new_score = self.on_access(entry.key, entry.value, entry.score) if new_score != entry.score: entry.score = new_score # changing the score of a pinned entry cannot unbalance the heap, as # we place all pinned entries after unpinned ones, regardless of score. if entry.pins == 0: self.__balance(i) def __swap(self, i: int, j: int) -> None: assert i < j assert self.data[j].sort_key < self.data[i].sort_key self.data[i], self.data[j] = self.data[j], self.data[i] self.keys_to_indices[self.data[i].key] = i self.keys_to_indices[self.data[j].key] = j def __balance(self, i: int) -> None: """When we have made a modification to the heap such that the heap property has been violated locally around i but previously held for all other indexes (and no other values have been modified), this fixes the heap so that the heap property holds everywhere.""" # bubble up (if score is too low for current position) while (parent := (i - 1) // 2) >= 0: if self.__out_of_order(parent, i): self.__swap(parent, i) i = parent else: break # or bubble down (if score is too high for current position) while children := [j for j in (2 * i + 1, 2 * i + 2) if j < len(self.data)]: smallest_child = min(children, key=lambda j: self.data[j].sort_key) if self.__out_of_order(i, smallest_child): self.__swap(i, smallest_child) i = smallest_child else: break def __out_of_order(self, i: int, j: int) -> bool: """Returns True if the indices i, j are in the wrong order. i must be the parent of j. """ assert i == (j - 1) // 2 return self.data[j].sort_key < self.data[i].sort_key class LRUReusedCache(GenericCache[K, V]): """The only concrete implementation of GenericCache we use outside of tests currently. Adopts a modified least-recently used eviction policy: It evicts the key that has been used least recently, but it will always preferentially evict keys that have never been accessed after insertion. Among keys that have been accessed, it ignores the number of accesses. This retains most of the benefits of an LRU cache, but adds an element of scan-resistance to the process: If we end up scanning through a large number of keys without reusing them, this does not evict the existing entries in preference for the new ones. """ __slots__ = ("__tick",) def __init__(self, max_size: int): super().__init__(max_size) self.__tick: int = 0 def tick(self) -> int: self.__tick += 1 return self.__tick def new_entry(self, key: K, value: V) -> Any: return (1, self.tick()) def on_access(self, key: K, value: V, score: Any) -> Any: return (2, self.tick()) class LRUCache(Generic[K, V]): """ This is a drop-in replacement for a GenericCache (despite the lack of inheritance) in performance critical environments. It turns out that GenericCache's heap balancing for arbitrary scores can be quite expensive compared to the doubly linked list approach of lru_cache or OrderedDict. This class is a pure LRU and does not provide any sort of affininty towards the number of accesses beyond recency. If soft-pinning entries which have been accessed at least once is important, use LRUReusedCache. """ # Here are some nice performance references for lru_cache vs OrderedDict: # https://github.com/python/cpython/issues/72426#issuecomment-1093727671 # https://discuss.python.org/t/simplify-lru-cache/18192/6 # # We use OrderedDict here because it is unclear to me we can provide the same # api as GenericCache using @lru_cache without messing with lru_cache internals. # # Anecdotally, OrderedDict seems quite competitive with lru_cache, but perhaps # that is localized to our access patterns. def __init__(self, max_size: int) -> None: assert max_size > 0 self.max_size = max_size self._threadlocal = threading.local() @property def cache(self) -> OrderedDict[K, V]: try: return self._threadlocal.cache except AttributeError: self._threadlocal.cache = OrderedDict() return self._threadlocal.cache def __setitem__(self, key: K, value: V) -> None: self.cache[key] = value self.cache.move_to_end(key) while len(self.cache) > self.max_size: self.cache.popitem(last=False) def __getitem__(self, key: K) -> V: val = self.cache[key] self.cache.move_to_end(key) return val def __iter__(self): return iter(self.cache) def __len__(self) -> int: return len(self.cache) def __contains__(self, key: K) -> bool: return key in self.cache # implement GenericCache interface, for tests def check_valid(self) -> None: pass ================================================ FILE: hypothesis-python/src/hypothesis/internal/cathetus.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from math import fabs, inf, isinf, isnan, nan, sqrt from sys import float_info def cathetus(h: float, a: float) -> float: """Given the lengths of the hypotenuse and a side of a right triangle, return the length of the other side. A companion to the C99 hypot() function. Some care is needed to avoid underflow in the case of small arguments, and overflow in the case of large arguments as would occur for the naive implementation as sqrt(h*h - a*a). The behaviour with respect the non-finite arguments (NaNs and infinities) is designed to be as consistent as possible with the C99 hypot() specifications. This function relies on the system ``sqrt`` function and so, like it, may be inaccurate up to a relative error of (around) floating-point epsilon. Based on the C99 implementation https://gitlab.com/jjg/cathetus """ if isnan(h): return nan if isinf(h): if isinf(a): return nan else: # Deliberately includes the case when isnan(a), because the # C99 standard mandates that hypot(inf, nan) == inf return inf h = fabs(h) a = fabs(a) if h < a: return nan # Thanks to floating-point precision issues when performing multiple # operations on extremely large or small values, we may rarely calculate # a side length that is longer than the hypotenuse. This is clearly an # error, so we clip to the hypotenuse as the best available estimate. if h > sqrt(float_info.max): if h > float_info.max / 2: b = sqrt(h - a) * sqrt(h / 2 + a / 2) * sqrt(2) else: b = sqrt(h - a) * sqrt(h + a) elif h < sqrt(float_info.min): b = sqrt(h - a) * sqrt(h + a) else: b = sqrt((h - a) * (h + a)) return min(b, h) ================================================ FILE: hypothesis-python/src/hypothesis/internal/charmap.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import codecs import gzip import json import os import sys import tempfile import unicodedata from collections.abc import Collection, Iterable from functools import cache from pathlib import Path from typing import Literal, TypeAlias from hypothesis.configuration import storage_directory from hypothesis.control import _current_build_context from hypothesis.errors import InvalidArgument from hypothesis.internal.intervalsets import IntervalSet, IntervalsT # See https://en.wikipedia.org/wiki/Unicode_character_property#General_Category CategoryName: TypeAlias = Literal[ "L", # Letter "Lu", # Letter, uppercase "Ll", # Letter, lowercase "Lt", # Letter, titlecase "Lm", # Letter, modifier "Lo", # Letter, other "M", # Mark "Mn", # Mark, nonspacing "Mc", # Mark, spacing combining "Me", # Mark, enclosing "N", # Number "Nd", # Number, decimal digit "Nl", # Number, letter "No", # Number, other "P", # Punctuation "Pc", # Punctuation, connector "Pd", # Punctuation, dash "Ps", # Punctuation, open "Pe", # Punctuation, close "Pi", # Punctuation, initial quote "Pf", # Punctuation, final quote "Po", # Punctuation, other "S", # Symbol "Sm", # Symbol, math "Sc", # Symbol, currency "Sk", # Symbol, modifier "So", # Symbol, other "Z", # Separator "Zs", # Separator, space "Zl", # Separator, line "Zp", # Separator, paragraph "C", # Other "Cc", # Other, control "Cf", # Other, format "Cs", # Other, surrogate "Co", # Other, private use "Cn", # Other, not assigned ] Categories: TypeAlias = Iterable[CategoryName] CategoriesTuple: TypeAlias = tuple[CategoryName, ...] def charmap_file(fname: str = "charmap") -> Path: return storage_directory( "unicode_data", unicodedata.unidata_version, f"{fname}.json.gz" ) _charmap: dict[CategoryName, IntervalsT] | None = None def charmap() -> dict[CategoryName, IntervalsT]: """Return a dict that maps a Unicode category, to a tuple of 2-tuples covering the codepoint intervals for characters in that category. >>> charmap()['Co'] ((57344, 63743), (983040, 1048573), (1048576, 1114109)) """ global _charmap # Best-effort caching in the face of missing files and/or unwritable # filesystems is fairly simple: check if loaded, else try loading, # else calculate and try writing the cache. if _charmap is None: f = charmap_file() try: with gzip.GzipFile(f, "rb") as d: tmp_charmap = dict(json.load(d)) except Exception: # This loop is reduced to using only local variables for performance; # indexing and updating containers is a ~3x slowdown. This doesn't fix # https://github.com/HypothesisWorks/hypothesis/issues/2108 but it helps. category = unicodedata.category # Local variable -> ~20% speedup! tmp_charmap = {} last_cat = category(chr(0)) last_start = 0 for i in range(1, sys.maxunicode + 1): cat = category(chr(i)) if cat != last_cat: tmp_charmap.setdefault(last_cat, []).append((last_start, i - 1)) last_cat, last_start = cat, i tmp_charmap.setdefault(last_cat, []).append((last_start, sys.maxunicode)) try: # Write the Unicode table atomically tmpdir = storage_directory("tmp") tmpdir.mkdir(exist_ok=True, parents=True) fd, tmpfile = tempfile.mkstemp(dir=tmpdir) os.close(fd) # Explicitly set the mtime to get reproducible output with gzip.GzipFile(tmpfile, "wb", mtime=1) as fp: result = json.dumps(sorted(tmp_charmap.items())) fp.write(result.encode()) os.renames(tmpfile, f) except Exception: pass # convert between lists and tuples _charmap = { k: tuple(tuple(pair) for pair in pairs) for k, pairs in tmp_charmap.items() } # each value is a tuple of 2-tuples (that is, tuples of length 2) # and both elements of that tuple are integers. for vs in _charmap.values(): ints = list(sum(vs, ())) assert all(isinstance(x, int) for x in ints) assert ints == sorted(ints) assert all(len(tup) == 2 for tup in vs) assert _charmap is not None return _charmap @cache def intervals_from_codec(codec_name: str) -> IntervalSet: # pragma: no cover """Return an IntervalSet of characters which are part of this codec.""" assert codec_name == codecs.lookup(codec_name).name fname = charmap_file(f"codec-{codec_name}") try: with gzip.GzipFile(fname) as gzf: encodable_intervals = json.load(gzf) except Exception: # This loop is kinda slow, but hopefully we don't need to do it very often! encodable_intervals = [] for i in range(sys.maxunicode + 1): try: chr(i).encode(codec_name) except Exception: # usually _but not always_ UnicodeEncodeError pass else: encodable_intervals.append((i, i)) res = IntervalSet(encodable_intervals) res = res.union(res) try: # Write the Unicode table atomically tmpdir = storage_directory("tmp") tmpdir.mkdir(exist_ok=True, parents=True) fd, tmpfile = tempfile.mkstemp(dir=tmpdir) os.close(fd) # Explicitly set the mtime to get reproducible output with gzip.GzipFile(tmpfile, "wb", mtime=1) as f: f.write(json.dumps(res.intervals).encode()) os.renames(tmpfile, fname) except Exception: pass return res _categories: Categories | None = None def categories() -> Categories: """Return a tuple of Unicode categories in a normalised order. >>> categories() # doctest: +ELLIPSIS ('Zl', 'Zp', 'Co', 'Me', 'Pc', ..., 'Cc', 'Cs') """ global _categories if _categories is None: cm = charmap() categories = sorted(cm.keys(), key=lambda c: len(cm[c])) categories.remove("Cc") # Other, Control categories.remove("Cs") # Other, Surrogate categories.append("Cc") categories.append("Cs") _categories = tuple(categories) return _categories def as_general_categories(cats: Categories, name: str = "cats") -> CategoriesTuple: """Return a tuple of Unicode categories in a normalised order. This function expands one-letter designations of a major class to include all subclasses: >>> as_general_categories(['N']) ('Nd', 'Nl', 'No') See section 4.5 of the Unicode standard for more on classes: https://www.unicode.org/versions/Unicode10.0.0/ch04.pdf If the collection ``cats`` includes any elements that do not represent a major class or a class with subclass, a deprecation warning is raised. """ major_classes = ("L", "M", "N", "P", "S", "Z", "C") cs = categories() out = set(cats) for c in cats: if c in major_classes: out.discard(c) out.update(x for x in cs if x.startswith(c)) elif c not in cs: raise InvalidArgument( f"In {name}={cats!r}, {c!r} is not a valid Unicode category." ) return tuple(c for c in cs if c in out) category_index_cache: dict[frozenset[CategoryName], IntervalsT] = {frozenset(): ()} def _category_key(cats: Iterable[str] | None) -> CategoriesTuple: """Return a normalised tuple of all Unicode categories that are in `include`, but not in `exclude`. If include is None then default to including all categories. Any item in include that is not a unicode character will be excluded. >>> _category_key(exclude=['So'], include=['Lu', 'Me', 'Cs', 'So']) ('Me', 'Lu', 'Cs') """ cs = categories() if cats is None: cats = set(cs) return tuple(c for c in cs if c in cats) def _query_for_key(key: Categories) -> IntervalsT: """Return a tuple of codepoint intervals covering characters that match one or more categories in the tuple of categories `key`. >>> _query_for_key(categories()) ((0, 1114111),) >>> _query_for_key(('Zl', 'Zp', 'Co')) ((8232, 8233), (57344, 63743), (983040, 1048573), (1048576, 1114109)) """ key = tuple(key) # ignore ordering on the cache key to increase potential cache hits. cache_key = frozenset(key) context = _current_build_context.value if context is None or not context.data.provider.avoid_realization: try: return category_index_cache[cache_key] except KeyError: pass elif not key: # pragma: no cover # only on alternative backends return () assert key if set(key) == set(categories()): result = IntervalSet([(0, sys.maxunicode)]) else: result = IntervalSet(_query_for_key(key[:-1])).union( IntervalSet(charmap()[key[-1]]) ) assert isinstance(result, IntervalSet) if context is None or not context.data.provider.avoid_realization: category_index_cache[cache_key] = result.intervals return result.intervals limited_category_index_cache: dict[ tuple[CategoriesTuple, int, int, IntervalsT, IntervalsT], IntervalSet ] = {} def query( *, categories: Categories | None = None, min_codepoint: int | None = None, max_codepoint: int | None = None, include_characters: Collection[str] = "", exclude_characters: Collection[str] = "", ) -> IntervalSet: """Return a tuple of intervals covering the codepoints for all characters that meet the criteria. >>> query() ((0, 1114111),) >>> query(min_codepoint=0, max_codepoint=128) ((0, 128),) >>> query(min_codepoint=0, max_codepoint=128, categories=['Lu']) ((65, 90),) >>> query(min_codepoint=0, max_codepoint=128, categories=['Lu'], ... include_characters='☃') ((65, 90), (9731, 9731)) """ if min_codepoint is None: min_codepoint = 0 if max_codepoint is None: max_codepoint = sys.maxunicode catkey = _category_key(categories) character_intervals = IntervalSet.from_string("".join(include_characters)) exclude_intervals = IntervalSet.from_string("".join(exclude_characters)) qkey = ( catkey, min_codepoint, max_codepoint, character_intervals.intervals, exclude_intervals.intervals, ) context = _current_build_context.value if context is None or not context.data.provider.avoid_realization: try: return limited_category_index_cache[qkey] except KeyError: pass base = _query_for_key(catkey) result = [] for u, v in base: if v >= min_codepoint and u <= max_codepoint: result.append((max(u, min_codepoint), min(v, max_codepoint))) result = (IntervalSet(result) | character_intervals) - exclude_intervals if context is None or not context.data.provider.avoid_realization: limited_category_index_cache[qkey] = result return result ================================================ FILE: hypothesis-python/src/hypothesis/internal/compat.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import codecs import copy import dataclasses import inspect import itertools import platform import sys import sysconfig import typing from functools import partial from typing import ( TYPE_CHECKING, Any, ForwardRef, Optional, TypedDict as TypedDict, get_args, ) if sys.version_info >= (3, 11): BaseExceptionGroup = BaseExceptionGroup # noqa: F821 ExceptionGroup = ExceptionGroup # noqa: F821 else: # pragma: no cover from exceptiongroup import ( BaseExceptionGroup as BaseExceptionGroup, ExceptionGroup as ExceptionGroup, ) if TYPE_CHECKING: from typing_extensions import ( NotRequired as NotRequired, TypedDict as TypedDict, override as override, ) from hypothesis.internal.conjecture.engine import ConjectureRunner else: # In order to use NotRequired, we need the version of TypedDict included in Python 3.11+. if sys.version_info[:2] >= (3, 11): from typing import NotRequired as NotRequired, TypedDict as TypedDict else: # pragma: no cover try: from typing_extensions import ( NotRequired as NotRequired, TypedDict as TypedDict, ) except ImportError: # We can use the old TypedDict from Python 3.8+ at runtime. class NotRequired: """A runtime placeholder for the NotRequired type, which is not available in Python <3.11.""" def __class_getitem__(cls, item): return cls try: from typing import ( override as override, ) except ImportError: # pragma: no cover try: from typing_extensions import ( override as override, ) except ImportError: override = lambda f: f PYPY = platform.python_implementation() == "PyPy" GRAALPY = platform.python_implementation() == "GraalVM" WINDOWS = platform.system() == "Windows" # First defined in CPython 3.13, defaults to False FREE_THREADED_CPYTHON = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) def add_note(exc, note): try: exc.add_note(note) except AttributeError: # pragma: no cover if not hasattr(exc, "__notes__"): try: exc.__notes__ = [] except AttributeError: return # give up, might be e.g. a frozen dataclass exc.__notes__.append(note) def escape_unicode_characters(s: str) -> str: return codecs.encode(s, "unicode_escape").decode("ascii") def int_from_bytes(data: bytes | bytearray) -> int: return int.from_bytes(data, "big") def int_to_bytes(i: int, size: int) -> bytes: return i.to_bytes(size, "big") def int_to_byte(i: int) -> bytes: return bytes([i]) def is_typed_named_tuple(cls: type) -> bool: """Return True if cls is probably a subtype of `typing.NamedTuple`. Unfortunately types created with `class T(NamedTuple):` actually subclass `tuple` directly rather than NamedTuple. This is annoying, and means we just have to hope that nobody defines a different tuple subclass with similar attributes. """ return ( issubclass(cls, tuple) and hasattr(cls, "_fields") and (hasattr(cls, "_field_types") or hasattr(cls, "__annotations__")) ) def _hint_and_args(x): return (x, *get_args(x)) def get_type_hints(thing: object) -> dict[str, Any]: """Like the typing version, but tries harder and never errors. Tries harder: if the thing to inspect is a class but typing.get_type_hints raises an error or returns no hints, then this function will try calling it on the __init__ method. This second step often helps with user-defined classes on older versions of Python. The third step we take is trying to fetch types from the __signature__ property. They override any other ones we found earlier. Never errors: instead of raising TypeError for uninspectable objects, or NameError for unresolvable forward references, just return an empty dict. """ if isinstance(thing, partial): from hypothesis.internal.reflection import get_signature bound = set(get_signature(thing.func).parameters).difference( get_signature(thing).parameters ) return {k: v for k, v in get_type_hints(thing.func).items() if k not in bound} try: hints = typing.get_type_hints(thing, include_extras=True) except (AttributeError, TypeError, NameError): # pragma: no cover hints = {} if inspect.isclass(thing): try: hints.update(typing.get_type_hints(thing.__init__, include_extras=True)) except (TypeError, NameError, AttributeError): pass try: if hasattr(thing, "__signature__"): # It is possible for the signature and annotations attributes to # differ on an object due to renamed arguments. from hypothesis.internal.reflection import get_signature from hypothesis.strategies._internal.types import is_a_type vkinds = (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) for p in get_signature(thing).parameters.values(): if ( p.kind not in vkinds and is_a_type(p.annotation) and p.annotation is not p.empty ): p_hint = p.annotation # Defer to `get_type_hints` if signature annotation is, or # contains, a forward reference that is otherwise resolved. if any( isinstance(sig_hint, ForwardRef) and not isinstance(hint, ForwardRef) for sig_hint, hint in zip( _hint_and_args(p.annotation), _hint_and_args(hints.get(p.name, Any)), strict=False, ) ): p_hint = hints[p.name] if p.default is None: hints[p.name] = p_hint | None else: hints[p.name] = p_hint except (AttributeError, TypeError, NameError): # pragma: no cover pass return hints # Under Python 2, math.floor and math.ceil returned floats, which cannot # represent large integers - eg `float(2**53) == float(2**53 + 1)`. # We therefore implement them entirely in (long) integer operations. # We still use the same trick on Python 3, because Numpy values and other # custom __floor__ or __ceil__ methods may convert via floats. # See issue #1667, Numpy issue 9068. def floor(x): y = int(x) if y != x and x < 0: return y - 1 return y def ceil(x): y = int(x) if y != x and x > 0: return y + 1 return y def extract_bits(x: int, /, width: int | None = None) -> list[int]: assert x >= 0 result = [] while x: result.append(x & 1) x >>= 1 if width is not None: result = (result + [0] * width)[:width] result.reverse() return result # int.bit_count was added in python 3.10 try: bit_count = int.bit_count except AttributeError: # pragma: no cover bit_count = lambda self: sum(extract_bits(abs(self))) def bad_django_TestCase(runner: Optional["ConjectureRunner"]) -> bool: if runner is None or "django.test" not in sys.modules: return False else: # pragma: no cover if not isinstance(runner, sys.modules["django.test"].TransactionTestCase): return False from hypothesis.extra.django._impl import HypothesisTestCase return not isinstance(runner, HypothesisTestCase) # see issue #3812 if sys.version_info[:2] < (3, 12): def _asdict_inner(obj, dict_factory): if dataclasses._is_dataclass_instance(obj): return dict_factory( (f.name, _asdict_inner(getattr(obj, f.name), dict_factory)) for f in dataclasses.fields(obj) ) elif isinstance(obj, tuple) and hasattr(obj, "_fields"): return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj]) elif isinstance(obj, (list, tuple)): return type(obj)(_asdict_inner(v, dict_factory) for v in obj) elif isinstance(obj, dict): if hasattr(type(obj), "default_factory"): result = type(obj)(obj.default_factory) for k, v in obj.items(): result[_asdict_inner(k, dict_factory)] = _asdict_inner( v, dict_factory ) return result return type(obj)( (_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory)) for k, v in obj.items() ) else: return copy.deepcopy(obj) def dataclass_asdict(obj, *, dict_factory=dict): """ A vendored variant of dataclasses.asdict. Includes the bugfix for defaultdicts (cpython/32056) for all versions. See also issues/3812. This should be removed whenever we drop support for 3.11. We can use the standard dataclasses.asdict after that point. """ if not dataclasses._is_dataclass_instance(obj): # pragma: no cover raise TypeError("asdict() should be called on dataclass instances") return _asdict_inner(obj, dict_factory) else: # pragma: no cover dataclass_asdict = dataclasses.asdict if sys.version_info[:2] < (3, 13): # batched was added in 3.12, strict flag in 3.13 # copied from 3.13 docs reference implementation def batched(iterable, n, *, strict=False): if n < 1: raise ValueError("n must be at least one") iterator = iter(iterable) while batch := tuple(itertools.islice(iterator, n)): if strict and len(batch) != n: # pragma: no cover raise ValueError("batched(): incomplete batch") yield batch else: # pragma: no cover batched = itertools.batched ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/choice.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from collections.abc import Callable, Hashable, Iterable, Sequence from dataclasses import dataclass from typing import ( Literal, TypeAlias, TypedDict, TypeVar, cast, ) from hypothesis.errors import ChoiceTooLarge from hypothesis.internal.conjecture.floats import float_to_lex, lex_to_float from hypothesis.internal.conjecture.utils import identity from hypothesis.internal.floats import float_to_int, make_float_clamper, sign_aware_lte from hypothesis.internal.intervalsets import IntervalSet T = TypeVar("T") class IntegerConstraints(TypedDict): min_value: int | None max_value: int | None weights: dict[int, float] | None shrink_towards: int class FloatConstraints(TypedDict): min_value: float max_value: float allow_nan: bool smallest_nonzero_magnitude: float class StringConstraints(TypedDict): intervals: IntervalSet min_size: int max_size: int class BytesConstraints(TypedDict): min_size: int max_size: int class BooleanConstraints(TypedDict): p: float ChoiceT: TypeAlias = int | str | bool | float | bytes ChoiceConstraintsT: TypeAlias = ( IntegerConstraints | FloatConstraints | StringConstraints | BytesConstraints | BooleanConstraints ) ChoiceTypeT: TypeAlias = Literal["integer", "string", "boolean", "float", "bytes"] ChoiceKeyT: TypeAlias = ( int | str | bytes | tuple[Literal["bool"], bool] | tuple[Literal["float"], int] ) @dataclass(slots=True, frozen=False) class ChoiceTemplate: type: Literal["simplest"] count: int | None def __post_init__(self) -> None: if self.count is not None: assert self.count > 0 @dataclass(slots=True, frozen=False) class ChoiceNode: type: ChoiceTypeT value: ChoiceT constraints: ChoiceConstraintsT was_forced: bool index: int | None = None def copy( self, *, with_value: ChoiceT | None = None, with_constraints: ChoiceConstraintsT | None = None, ) -> "ChoiceNode": # we may want to allow this combination in the future, but for now it's # a footgun. if self.was_forced: assert with_value is None, "modifying a forced node doesn't make sense" # explicitly not copying index. node indices are only assigned via # ExampleRecord. This prevents footguns with relying on stale indices # after copying. return ChoiceNode( type=self.type, value=self.value if with_value is None else with_value, constraints=( self.constraints if with_constraints is None else with_constraints ), was_forced=self.was_forced, ) @property def trivial(self) -> bool: """ A node is trivial if it cannot be simplified any further. This does not mean that modifying a trivial node can't produce simpler test cases when viewing the tree as a whole. Just that when viewing this node in isolation, this is the simplest the node can get. """ if self.was_forced: return True if self.type != "float": zero_value = choice_from_index(0, self.type, self.constraints) return choice_equal(self.value, zero_value) else: constraints = cast(FloatConstraints, self.constraints) min_value = constraints["min_value"] max_value = constraints["max_value"] shrink_towards = 0.0 if min_value == -math.inf and max_value == math.inf: return choice_equal(self.value, shrink_towards) if ( not math.isinf(min_value) and not math.isinf(max_value) and math.ceil(min_value) <= math.floor(max_value) ): # the interval contains an integer. the simplest integer is the # one closest to shrink_towards shrink_towards = max(math.ceil(min_value), shrink_towards) shrink_towards = min(math.floor(max_value), shrink_towards) return choice_equal(self.value, float(shrink_towards)) # the real answer here is "the value in [min_value, max_value] with # the lowest denominator when represented as a fraction". # It would be good to compute this correctly in the future, but it's # also not incorrect to be conservative here. return False def __eq__(self, other: object) -> bool: if not isinstance(other, ChoiceNode): return NotImplemented return ( self.type == other.type and choice_equal(self.value, other.value) and choice_constraints_equal(self.type, self.constraints, other.constraints) and self.was_forced == other.was_forced ) def __hash__(self) -> int: return hash( ( self.type, choice_key(self.value), choice_constraints_key(self.type, self.constraints), self.was_forced, ) ) def __repr__(self) -> str: forced_marker = " [forced]" if self.was_forced else "" return f"{self.type} {self.value!r}{forced_marker} {self.constraints!r}" def _size_to_index(size: int, *, alphabet_size: int) -> int: # this is the closed form of this geometric series: # for i in range(size): # index += alphabet_size**i if alphabet_size <= 0: assert size == 0 return 0 if alphabet_size == 1: return size v = (alphabet_size**size - 1) // (alphabet_size - 1) # mypy thinks (m: int) // (n: int) -> Any. assert it back to int. return cast(int, v) def _index_to_size(index: int, alphabet_size: int) -> int: if alphabet_size == 0: return 0 elif alphabet_size == 1: # there is only one string of each size, so the size is equal to its # ordering. return index # the closed-form inverse of _size_to_index is # size = math.floor(math.log(index * (alphabet_size - 1) + 1, alphabet_size)) # which is fast, but suffers from float precision errors. As performance is # relatively critical here, we'll use this formula by default, but fall back to # a much slower integer-only logarithm when the calculation is too close for # comfort. total = index * (alphabet_size - 1) + 1 size = math.log(total, alphabet_size) # if this computation is close enough that it could have been affected by # floating point errors, use a much slower integer-only logarithm instead, # which is guaranteed to be precise. if 0 < math.ceil(size) - size < 1e-7: s = 0 while total >= alphabet_size: total //= alphabet_size s += 1 return s return math.floor(size) def collection_index( choice: Sequence[T], *, min_size: int, alphabet_size: int, to_order: Callable[[T], int], ) -> int: # Collections are ordered by counting the number of values of each size, # starting with min_size. alphabet_size indicates how many options there # are for a single element. to_order orders an element by returning an n ≥ 0. # we start by adding the size to the index, relative to min_size. index = _size_to_index(len(choice), alphabet_size=alphabet_size) - _size_to_index( min_size, alphabet_size=alphabet_size ) # We then add each element c to the index, starting from the end (so "ab" is # simpler than "ba"). Each loop takes c at position i in the sequence and # computes the number of sequences of size i which come before it in the ordering. # this running_exp computation is equivalent to doing # index += (alphabet_size**i) * n # but reuses intermediate exponentiation steps for efficiency. running_exp = 1 for c in reversed(choice): index += running_exp * to_order(c) running_exp *= alphabet_size return index def collection_value( index: int, *, min_size: int, alphabet_size: int, from_order: Callable[[int], T], ) -> list[T]: from hypothesis.internal.conjecture.engine import BUFFER_SIZE # this function is probably easiest to make sense of as an inverse of # collection_index, tracking ~corresponding lines of code between the two. index += _size_to_index(min_size, alphabet_size=alphabet_size) size = _index_to_size(index, alphabet_size=alphabet_size) # index -> value computation can be arbitrarily expensive for arbitrarily # large min_size collections. short-circuit if the resulting size would be # obviously-too-large. callers will generally turn this into a .mark_overrun(). if size >= BUFFER_SIZE: raise ChoiceTooLarge # subtract out the amount responsible for the size index -= _size_to_index(size, alphabet_size=alphabet_size) vals: list[T] = [] for i in reversed(range(size)): # optimization for common case when we hit index 0. Exponentiation # on large integers is expensive! if index == 0: n = 0 else: n = index // (alphabet_size**i) # subtract out the nearest multiple of alphabet_size**i index -= n * (alphabet_size**i) vals.append(from_order(n)) return vals def zigzag_index(value: int, *, shrink_towards: int) -> int: # value | 0 1 -1 2 -2 3 -3 4 # index | 0 1 2 3 4 5 6 7 index = 2 * abs(shrink_towards - value) if value > shrink_towards: index -= 1 return index def zigzag_value(index: int, *, shrink_towards: int) -> int: assert index >= 0 # count how many "steps" away from shrink_towards we are. n = (index + 1) // 2 # now check if we're stepping up or down from shrink_towards. if (index % 2) == 0: n *= -1 return shrink_towards + n def choice_to_index(choice: ChoiceT, constraints: ChoiceConstraintsT) -> int: # This function takes a choice in the choice sequence and returns the # complexity index of that choice from among its possible values, where 0 # is the simplest. # # Note that the index of a choice depends on its constraints. The simplest value # (at index 0) for {"min_value": None, "max_value": None} is 0, while for # {"min_value": 1, "max_value": None} the simplest value is 1. # # choice_from_index inverts this function. An invariant on both functions is # that they must be injective. Unfortunately, floats do not currently respect # this. That's not *good*, but nothing has blown up - yet. And ordering # floats in a sane manner is quite hard, so I've left it for another day. if isinstance(choice, int) and not isinstance(choice, bool): # Let a = shrink_towards. # * Unbounded: Ordered by (|a - x|, sgn(a - x)). Think of a zigzag. # [a, a + 1, a - 1, a + 2, a - 2, ...] # * Semi-bounded: Same as unbounded, except stop on one side when you hit # {min, max}_value. so min_value=-1 a=0 has order # [0, 1, -1, 2, 3, 4, ...] # * Bounded: Same as unbounded and semibounded, except stop on each side # when you hit {min, max}_value. # # To simplify and gain intuition about this ordering, you can think about # the most common case where 0 is first (a = 0). We deviate from this only # rarely, e.g. for datetimes, where we generally want year 2000 to be # simpler than year 0. constraints = cast(IntegerConstraints, constraints) shrink_towards = constraints["shrink_towards"] min_value = constraints["min_value"] max_value = constraints["max_value"] if min_value is not None: shrink_towards = max(min_value, shrink_towards) if max_value is not None: shrink_towards = min(max_value, shrink_towards) if min_value is None and max_value is None: # case: unbounded return zigzag_index(choice, shrink_towards=shrink_towards) elif min_value is not None and max_value is None: # case: semibounded below # min_value = -2 # index | 0 1 2 3 4 5 6 7 # v | 0 1 -1 2 -2 3 4 5 if abs(choice - shrink_towards) <= (shrink_towards - min_value): return zigzag_index(choice, shrink_towards=shrink_towards) return choice - min_value elif max_value is not None and min_value is None: # case: semibounded above if abs(choice - shrink_towards) <= (max_value - shrink_towards): return zigzag_index(choice, shrink_towards=shrink_towards) return max_value - choice else: # case: bounded # range = [-2, 5] # shrink_towards = 2 # index | 0 1 2 3 4 5 6 7 # v | 2 3 1 4 0 5 -1 -2 # # ^ with zero weights at index = [0, 2, 6] # index | 0 1 2 3 4 # v | 3 4 0 5 -2 assert min_value is not None assert max_value is not None assert constraints["weights"] is None or all( w > 0 for w in constraints["weights"].values() ), "technically possible but really annoying to support zero weights" # check which side gets exhausted first if (shrink_towards - min_value) < (max_value - shrink_towards): # Below shrink_towards gets exhausted first. Equivalent to # semibounded below if abs(choice - shrink_towards) <= (shrink_towards - min_value): return zigzag_index(choice, shrink_towards=shrink_towards) return choice - min_value else: # Above shrink_towards gets exhausted first. Equivalent to semibounded # above if abs(choice - shrink_towards) <= (max_value - shrink_towards): return zigzag_index(choice, shrink_towards=shrink_towards) return max_value - choice elif isinstance(choice, bool): constraints = cast(BooleanConstraints, constraints) # Ordered by [False, True]. p = constraints["p"] if not (2 ** (-64) < p < (1 - 2 ** (-64))): # only one option is possible, so whatever it is is first. return 0 return int(choice) elif isinstance(choice, bytes): constraints = cast(BytesConstraints, constraints) return collection_index( list(choice), min_size=constraints["min_size"], alphabet_size=2**8, to_order=identity, ) elif isinstance(choice, str): constraints = cast(StringConstraints, constraints) intervals = constraints["intervals"] return collection_index( choice, min_size=constraints["min_size"], alphabet_size=len(intervals), to_order=intervals.index_from_char_in_shrink_order, ) elif isinstance(choice, float): sign = int(math.copysign(1.0, choice) < 0) return (sign << 64) | float_to_lex(abs(choice)) else: raise NotImplementedError def choice_from_index( index: int, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT ) -> ChoiceT: assert index >= 0 if choice_type == "integer": constraints = cast(IntegerConstraints, constraints) shrink_towards = constraints["shrink_towards"] min_value = constraints["min_value"] max_value = constraints["max_value"] if min_value is not None: shrink_towards = max(min_value, shrink_towards) if max_value is not None: shrink_towards = min(max_value, shrink_towards) if min_value is None and max_value is None: # case: unbounded return zigzag_value(index, shrink_towards=shrink_towards) elif min_value is not None and max_value is None: # case: semibounded below if index <= zigzag_index(min_value, shrink_towards=shrink_towards): return zigzag_value(index, shrink_towards=shrink_towards) return index + min_value elif max_value is not None and min_value is None: # case: semibounded above if index <= zigzag_index(max_value, shrink_towards=shrink_towards): return zigzag_value(index, shrink_towards=shrink_towards) return max_value - index else: # case: bounded assert min_value is not None assert max_value is not None assert constraints["weights"] is None or all( w > 0 for w in constraints["weights"].values() ), "possible but really annoying to support zero weights" if (shrink_towards - min_value) < (max_value - shrink_towards): # equivalent to semibounded below case if index <= zigzag_index(min_value, shrink_towards=shrink_towards): return zigzag_value(index, shrink_towards=shrink_towards) return index + min_value else: # equivalent to semibounded above case if index <= zigzag_index(max_value, shrink_towards=shrink_towards): return zigzag_value(index, shrink_towards=shrink_towards) return max_value - index elif choice_type == "boolean": constraints = cast(BooleanConstraints, constraints) # Ordered by [False, True]. p = constraints["p"] only = None if p <= 2 ** (-64): only = False elif p >= (1 - 2 ** (-64)): only = True assert index in {0, 1} if only is not None: # only one choice assert index == 0 return only return bool(index) elif choice_type == "bytes": constraints = cast(BytesConstraints, constraints) value_b = collection_value( index, min_size=constraints["min_size"], alphabet_size=2**8, from_order=identity, ) return bytes(value_b) elif choice_type == "string": constraints = cast(StringConstraints, constraints) intervals = constraints["intervals"] # _s because mypy is unhappy with reusing different-typed names in branches, # even if the branches are disjoint. value_s = collection_value( index, min_size=constraints["min_size"], alphabet_size=len(intervals), from_order=intervals.char_in_shrink_order, ) return "".join(value_s) elif choice_type == "float": constraints = cast(FloatConstraints, constraints) sign = -1 if index >> 64 else 1 result = sign * lex_to_float(index & ((1 << 64) - 1)) clamper = make_float_clamper( min_value=constraints["min_value"], max_value=constraints["max_value"], smallest_nonzero_magnitude=constraints["smallest_nonzero_magnitude"], allow_nan=constraints["allow_nan"], ) return clamper(result) else: raise NotImplementedError def choice_permitted(choice: ChoiceT, constraints: ChoiceConstraintsT) -> bool: if isinstance(choice, int) and not isinstance(choice, bool): constraints = cast(IntegerConstraints, constraints) min_value = constraints["min_value"] max_value = constraints["max_value"] if min_value is not None and choice < min_value: return False return not (max_value is not None and choice > max_value) elif isinstance(choice, float): constraints = cast(FloatConstraints, constraints) if math.isnan(choice): return constraints["allow_nan"] if 0 < abs(choice) < constraints["smallest_nonzero_magnitude"]: return False return sign_aware_lte(constraints["min_value"], choice) and sign_aware_lte( choice, constraints["max_value"] ) elif isinstance(choice, str): constraints = cast(StringConstraints, constraints) if len(choice) < constraints["min_size"]: return False if ( constraints["max_size"] is not None and len(choice) > constraints["max_size"] ): return False return all(ord(c) in constraints["intervals"] for c in choice) elif isinstance(choice, bytes): constraints = cast(BytesConstraints, constraints) if len(choice) < constraints["min_size"]: return False return constraints["max_size"] is None or len(choice) <= constraints["max_size"] elif isinstance(choice, bool): constraints = cast(BooleanConstraints, constraints) if constraints["p"] <= 0: return choice is False if constraints["p"] >= 1: return choice is True return True else: raise NotImplementedError(f"unhandled type {type(choice)} with value {choice}") def choices_key(choices: Sequence[ChoiceT]) -> tuple[ChoiceKeyT, ...]: return tuple(choice_key(choice) for choice in choices) def choice_key(choice: ChoiceT) -> ChoiceKeyT: if isinstance(choice, float): # float_to_int to distinguish -0.0/0.0, signaling/nonsignaling nans, etc, # and then add a "float" key to avoid colliding with actual integers. return ("float", float_to_int(choice)) if isinstance(choice, bool): # avoid choice_key(0) == choice_key(False) return ("bool", choice) return choice def choice_equal(choice1: ChoiceT, choice2: ChoiceT) -> bool: assert type(choice1) is type(choice2), (choice1, choice2) return choice_key(choice1) == choice_key(choice2) def choice_constraints_equal( choice_type: ChoiceTypeT, constraints1: ChoiceConstraintsT, constraints2: ChoiceConstraintsT, ) -> bool: return choice_constraints_key(choice_type, constraints1) == choice_constraints_key( choice_type, constraints2 ) def choice_constraints_key( choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT ) -> tuple[Hashable, ...]: if choice_type == "float": constraints = cast(FloatConstraints, constraints) return ( float_to_int(constraints["min_value"]), float_to_int(constraints["max_value"]), constraints["allow_nan"], constraints["smallest_nonzero_magnitude"], ) if choice_type == "integer": constraints = cast(IntegerConstraints, constraints) return ( constraints["min_value"], constraints["max_value"], None if constraints["weights"] is None else tuple(constraints["weights"]), constraints["shrink_towards"], ) return tuple(constraints[key] for key in sorted(constraints)) # type: ignore def choices_size(choices: Iterable[ChoiceT]) -> int: from hypothesis.database import choices_to_bytes return len(choices_to_bytes(choices)) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/data.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import time from collections import defaultdict from collections.abc import Hashable, Iterable, Iterator, Sequence from dataclasses import dataclass, field from enum import IntEnum from functools import cached_property from random import Random from typing import ( TYPE_CHECKING, Any, Literal, NoReturn, TypeAlias, TypeVar, cast, overload, ) from hypothesis.errors import ( CannotProceedScopeT, ChoiceTooLarge, Frozen, InvalidArgument, StopTest, ) from hypothesis.internal.cache import LRUCache from hypothesis.internal.compat import add_note from hypothesis.internal.conjecture.choice import ( BooleanConstraints, BytesConstraints, ChoiceConstraintsT, ChoiceNode, ChoiceT, ChoiceTemplate, ChoiceTypeT, FloatConstraints, IntegerConstraints, StringConstraints, choice_constraints_key, choice_from_index, choice_permitted, choices_size, ) from hypothesis.internal.conjecture.junkdrawer import IntList, gc_cumulative_time from hypothesis.internal.conjecture.providers import ( COLLECTION_DEFAULT_MAX_SIZE, HypothesisProvider, PrimitiveProvider, ) from hypothesis.internal.conjecture.utils import calc_label_from_name from hypothesis.internal.escalation import InterestingOrigin from hypothesis.internal.floats import ( SMALLEST_SUBNORMAL, float_to_int, int_to_float, sign_aware_lte, ) from hypothesis.internal.intervalsets import IntervalSet from hypothesis.internal.observability import PredicateCounts from hypothesis.reporting import debug_report from hypothesis.utils.conventions import not_set from hypothesis.utils.deprecation import note_deprecation from hypothesis.utils.threading import ThreadLocal if TYPE_CHECKING: from hypothesis.strategies import SearchStrategy from hypothesis.strategies._internal.core import DataObject from hypothesis.strategies._internal.random import RandomState from hypothesis.strategies._internal.strategies import Ex def __getattr__(name: str) -> Any: if name == "AVAILABLE_PROVIDERS": from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS note_deprecation( "hypothesis.internal.conjecture.data.AVAILABLE_PROVIDERS has been moved to " "hypothesis.internal.conjecture.providers.AVAILABLE_PROVIDERS.", since="2025-01-25", has_codemod=False, stacklevel=1, ) return AVAILABLE_PROVIDERS raise AttributeError( f"Module 'hypothesis.internal.conjecture.data' has no attribute {name}" ) T = TypeVar("T") TargetObservations = dict[str, int | float] # index, choice_type, constraints, forced value MisalignedAt: TypeAlias = tuple[int, ChoiceTypeT, ChoiceConstraintsT, ChoiceT | None] TOP_LABEL = calc_label_from_name("top") MAX_DEPTH = 100 threadlocal = ThreadLocal(global_test_counter=int) class Status(IntEnum): OVERRUN = 0 INVALID = 1 VALID = 2 INTERESTING = 3 def __repr__(self) -> str: return f"Status.{self.name}" @dataclass(slots=True, frozen=True) class StructuralCoverageTag: label: int STRUCTURAL_COVERAGE_CACHE: dict[int, StructuralCoverageTag] = {} def structural_coverage(label: int) -> StructuralCoverageTag: try: return STRUCTURAL_COVERAGE_CACHE[label] except KeyError: return STRUCTURAL_COVERAGE_CACHE.setdefault(label, StructuralCoverageTag(label)) # This cache can be quite hot and so we prefer LRUCache over LRUReusedCache for # performance. We lose scan resistance, but that's probably fine here. POOLED_CONSTRAINTS_CACHE: LRUCache[tuple[Any, ...], ChoiceConstraintsT] = LRUCache(4096) class Span: """A span tracks the hierarchical structure of choices within a single test run. Spans are created to mark regions of the choice sequence that are logically related to each other. For instance, Hypothesis tracks: - A single top-level span for the entire choice sequence - A span for the choices made by each strategy - Some strategies define additional spans within their choices. For instance, st.lists() tracks the "should add another element" choice and the "add another element" choices as separate spans. Spans provide useful information to the shrinker, mutator, targeted PBT, and other subsystems of Hypothesis. Rather than store each ``Span`` as a rich object, it is actually just an index into the ``Spans`` class defined below. This has two purposes: Firstly, for most properties of spans we will never need to allocate storage at all, because most properties are not used on most spans. Secondly, by storing the spans as compact lists of integers, we save a considerable amount of space compared to Python's normal object size. This does have the downside that it increases the amount of allocation we do, and slows things down as a result, in some usage patterns because we repeatedly allocate the same Span or int objects, but it will often dramatically reduce our memory usage, so is worth it. """ __slots__ = ("index", "owner") def __init__(self, owner: "Spans", index: int) -> None: self.owner = owner self.index = index def __eq__(self, other: object) -> bool: if self is other: return True if not isinstance(other, Span): return NotImplemented return (self.owner is other.owner) and (self.index == other.index) def __ne__(self, other: object) -> bool: if self is other: return False if not isinstance(other, Span): return NotImplemented return (self.owner is not other.owner) or (self.index != other.index) def __repr__(self) -> str: return f"spans[{self.index}]" @property def label(self) -> int: """A label is an opaque value that associates each span with its approximate origin, such as a particular strategy class or a particular kind of draw.""" return self.owner.labels[self.owner.label_indices[self.index]] @property def parent(self) -> int | None: """The index of the span that this one is nested directly within.""" if self.index == 0: return None return self.owner.parentage[self.index] @property def start(self) -> int: return self.owner.starts[self.index] @property def end(self) -> int: return self.owner.ends[self.index] @property def depth(self) -> int: """ Depth of this span in the span tree. The top-level span has a depth of 0. """ return self.owner.depths[self.index] @property def discarded(self) -> bool: """True if this is span's ``stop_span`` call had ``discard`` set to ``True``. This means we believe that the shrinker should be able to delete this span completely, without affecting the value produced by its enclosing strategy. Typically set when a rejection sampler decides to reject a generated value and try again.""" return self.index in self.owner.discarded @property def choice_count(self) -> int: """The number of choices in this span.""" return self.end - self.start @property def children(self) -> "list[Span]": """The list of all spans with this as a parent, in increasing index order.""" return [self.owner[i] for i in self.owner.children[self.index]] class SpanProperty: """There are many properties of spans that we calculate by essentially rerunning the test case multiple times based on the calls which we record in SpanProperty. This class defines a visitor, subclasses of which can be used to calculate these properties. """ def __init__(self, spans: "Spans"): self.span_stack: list[int] = [] self.spans = spans self.span_count = 0 self.choice_count = 0 def run(self) -> Any: """Rerun the test case with this visitor and return the results of ``self.finish()``.""" for record in self.spans.trail: if record == TrailType.STOP_SPAN_DISCARD: self.__pop(discarded=True) elif record == TrailType.STOP_SPAN_NO_DISCARD: self.__pop(discarded=False) elif record == TrailType.CHOICE: self.choice_count += 1 else: # everything after TrailType.CHOICE is the label of a span start. self.__push(record - TrailType.CHOICE - 1) return self.finish() def __push(self, label_index: int) -> None: i = self.span_count assert i < len(self.spans) self.start_span(i, label_index=label_index) self.span_count += 1 self.span_stack.append(i) def __pop(self, *, discarded: bool) -> None: i = self.span_stack.pop() self.stop_span(i, discarded=discarded) def start_span(self, i: int, label_index: int) -> None: """Called at the start of each span, with ``i`` the index of the span and ``label_index`` the index of its label in ``self.spans.labels``.""" def stop_span(self, i: int, *, discarded: bool) -> None: """Called at the end of each span, with ``i`` the index of the span and ``discarded`` being ``True`` if ``stop_span`` was called with ``discard=True``.""" def finish(self) -> Any: raise NotImplementedError class TrailType(IntEnum): STOP_SPAN_DISCARD = 1 STOP_SPAN_NO_DISCARD = 2 CHOICE = 3 # every trail element larger than TrailType.CHOICE is the label of a span # start, offset by its index. So the first span label is stored as 4, the # second as 5, etc, regardless of its actual integer label. class SpanRecord: """Records the series of ``start_span``, ``stop_span``, and ``draw_bits`` calls so that these may be stored in ``Spans`` and replayed when we need to know about the structure of individual ``Span`` objects. Note that there is significant similarity between this class and ``DataObserver``, and the plan is to eventually unify them, but they currently have slightly different functions and implementations. """ def __init__(self) -> None: self.labels: list[int] = [] self.__index_of_labels: dict[int, int] | None = {} self.trail = IntList() self.nodes: list[ChoiceNode] = [] def freeze(self) -> None: self.__index_of_labels = None def record_choice(self) -> None: self.trail.append(TrailType.CHOICE) def start_span(self, label: int) -> None: assert self.__index_of_labels is not None try: i = self.__index_of_labels[label] except KeyError: i = self.__index_of_labels.setdefault(label, len(self.labels)) self.labels.append(label) self.trail.append(TrailType.CHOICE + 1 + i) def stop_span(self, *, discard: bool) -> None: if discard: self.trail.append(TrailType.STOP_SPAN_DISCARD) else: self.trail.append(TrailType.STOP_SPAN_NO_DISCARD) class _starts_and_ends(SpanProperty): def __init__(self, spans: "Spans") -> None: super().__init__(spans) self.starts = IntList.of_length(len(self.spans)) self.ends = IntList.of_length(len(self.spans)) def start_span(self, i: int, label_index: int) -> None: self.starts[i] = self.choice_count def stop_span(self, i: int, *, discarded: bool) -> None: self.ends[i] = self.choice_count def finish(self) -> tuple[IntList, IntList]: return (self.starts, self.ends) class _discarded(SpanProperty): def __init__(self, spans: "Spans") -> None: super().__init__(spans) self.result: set[int] = set() def finish(self) -> frozenset[int]: return frozenset(self.result) def stop_span(self, i: int, *, discarded: bool) -> None: if discarded: self.result.add(i) class _parentage(SpanProperty): def __init__(self, spans: "Spans") -> None: super().__init__(spans) self.result = IntList.of_length(len(self.spans)) def stop_span(self, i: int, *, discarded: bool) -> None: if i > 0: self.result[i] = self.span_stack[-1] def finish(self) -> IntList: return self.result class _depths(SpanProperty): def __init__(self, spans: "Spans") -> None: super().__init__(spans) self.result = IntList.of_length(len(self.spans)) def start_span(self, i: int, label_index: int) -> None: self.result[i] = len(self.span_stack) def finish(self) -> IntList: return self.result class _label_indices(SpanProperty): def __init__(self, spans: "Spans") -> None: super().__init__(spans) self.result = IntList.of_length(len(self.spans)) def start_span(self, i: int, label_index: int) -> None: self.result[i] = label_index def finish(self) -> IntList: return self.result class _mutator_groups(SpanProperty): def __init__(self, spans: "Spans") -> None: super().__init__(spans) self.groups: dict[int, set[tuple[int, int]]] = defaultdict(set) def start_span(self, i: int, label_index: int) -> None: # TODO should we discard start == end cases? occurs for eg st.data() # which is conditionally or never drawn from. arguably swapping # nodes with the empty list is a useful mutation enabled by start == end? key = (self.spans[i].start, self.spans[i].end) self.groups[label_index].add(key) def finish(self) -> Iterable[set[tuple[int, int]]]: # Discard groups with only one span, since the mutator can't # do anything useful with them. return [g for g in self.groups.values() if len(g) >= 2] class Spans: """A lazy collection of ``Span`` objects, derived from the record of recorded behaviour in ``SpanRecord``. Behaves logically as if it were a list of ``Span`` objects, but actually mostly exists as a compact store of information for them to reference into. All properties on here are best understood as the backing storage for ``Span`` and are described there. """ def __init__(self, record: SpanRecord) -> None: self.trail = record.trail self.labels = record.labels self.__length = self.trail.count( TrailType.STOP_SPAN_DISCARD ) + record.trail.count(TrailType.STOP_SPAN_NO_DISCARD) self.__children: list[Sequence[int]] | None = None @cached_property def starts_and_ends(self) -> tuple[IntList, IntList]: return _starts_and_ends(self).run() @property def starts(self) -> IntList: return self.starts_and_ends[0] @property def ends(self) -> IntList: return self.starts_and_ends[1] @cached_property def discarded(self) -> frozenset[int]: return _discarded(self).run() @cached_property def parentage(self) -> IntList: return _parentage(self).run() @cached_property def depths(self) -> IntList: return _depths(self).run() @cached_property def label_indices(self) -> IntList: return _label_indices(self).run() @cached_property def mutator_groups(self) -> list[set[tuple[int, int]]]: return _mutator_groups(self).run() @property def children(self) -> list[Sequence[int]]: if self.__children is None: children = [IntList() for _ in range(len(self))] for i, p in enumerate(self.parentage): if i > 0: children[p].append(i) # Replace empty children lists with a tuple to reduce # memory usage. for i, c in enumerate(children): if not c: children[i] = () # type: ignore self.__children = children # type: ignore return self.__children # type: ignore def __len__(self) -> int: return self.__length def __getitem__(self, i: int) -> Span: n = self.__length if i < -n or i >= n: raise IndexError(f"Index {i} out of range [-{n}, {n})") if i < 0: i += n return Span(self, i) # not strictly necessary as we have len/getitem, but required for mypy. # https://github.com/python/mypy/issues/9737 def __iter__(self) -> Iterator[Span]: for i in range(len(self)): yield self[i] class _Overrun: status: Status = Status.OVERRUN def __repr__(self) -> str: return "Overrun" Overrun = _Overrun() class DataObserver: """Observer class for recording the behaviour of a ConjectureData object, primarily used for tracking the behaviour in the tree cache.""" def conclude_test( self, status: Status, interesting_origin: InterestingOrigin | None, ) -> None: """Called when ``conclude_test`` is called on the observed ``ConjectureData``, with the same arguments. Note that this is called after ``freeze`` has completed. """ def kill_branch(self) -> None: """Mark this part of the tree as not worth re-exploring.""" def draw_integer( self, value: int, *, constraints: IntegerConstraints, was_forced: bool ) -> None: pass def draw_float( self, value: float, *, constraints: FloatConstraints, was_forced: bool ) -> None: pass def draw_string( self, value: str, *, constraints: StringConstraints, was_forced: bool ) -> None: pass def draw_bytes( self, value: bytes, *, constraints: BytesConstraints, was_forced: bool ) -> None: pass def draw_boolean( self, value: bool, *, constraints: BooleanConstraints, was_forced: bool ) -> None: pass @dataclass(slots=True, frozen=True) class ConjectureResult: """Result class storing the parts of ConjectureData that we will care about after the original ConjectureData has outlived its usefulness.""" status: Status interesting_origin: InterestingOrigin | None nodes: tuple[ChoiceNode, ...] = field(repr=False, compare=False) length: int output: str expected_exception: BaseException | None expected_traceback: str | None has_discards: bool target_observations: TargetObservations tags: frozenset[StructuralCoverageTag] spans: Spans = field(repr=False, compare=False) arg_slices: set[tuple[int, int]] = field(repr=False) slice_comments: dict[tuple[int, int], str] = field(repr=False) misaligned_at: MisalignedAt | None = field(repr=False) cannot_proceed_scope: CannotProceedScopeT | None = field(repr=False) def as_result(self) -> "ConjectureResult": return self @property def choices(self) -> tuple[ChoiceT, ...]: return tuple(node.value for node in self.nodes) class ConjectureData: @classmethod def for_choices( cls, choices: Sequence[ChoiceTemplate | ChoiceT], *, observer: DataObserver | None = None, provider: PrimitiveProvider | type[PrimitiveProvider] = HypothesisProvider, random: Random | None = None, ) -> "ConjectureData": from hypothesis.internal.conjecture.engine import choice_count return cls( max_choices=choice_count(choices), random=random, prefix=choices, observer=observer, provider=provider, ) def __init__( self, *, random: Random | None, observer: DataObserver | None = None, provider: PrimitiveProvider | type[PrimitiveProvider] = HypothesisProvider, prefix: Sequence[ChoiceTemplate | ChoiceT] | None = None, max_choices: int | None = None, provider_kw: dict[str, Any] | None = None, ) -> None: from hypothesis.internal.conjecture.engine import BUFFER_SIZE if observer is None: observer = DataObserver() if provider_kw is None: provider_kw = {} elif not isinstance(provider, type): raise InvalidArgument( f"Expected {provider=} to be a class since {provider_kw=} was " "passed, but got an instance instead." ) assert isinstance(observer, DataObserver) self.observer = observer self.max_choices = max_choices self.max_length = BUFFER_SIZE self.overdraw = 0 self._random = random self.length: int = 0 self.index: int = 0 self.output: str = "" self.status: Status = Status.VALID self.frozen: bool = False self.testcounter: int = threadlocal.global_test_counter threadlocal.global_test_counter += 1 self.start_time = time.perf_counter() self.gc_start_time = gc_cumulative_time() self.events: dict[str, str | int | float] = {} self.interesting_origin: InterestingOrigin | None = None self.draw_times: dict[str, float] = {} self._stateful_run_times: dict[str, float] = defaultdict(float) self.max_depth: int = 0 self.has_discards: bool = False self.provider: PrimitiveProvider = ( provider(self, **provider_kw) if isinstance(provider, type) else provider ) assert isinstance(self.provider, PrimitiveProvider) self.__result: ConjectureResult | None = None # Observations used for targeted search. They'll be aggregated in # ConjectureRunner.generate_new_examples and fed to TargetSelector. self.target_observations: TargetObservations = {} # Tags which indicate something about which part of the search space # this example is in. These are used to guide generation. self.tags: set[StructuralCoverageTag] = set() self.labels_for_structure_stack: list[set[int]] = [] # Normally unpopulated but we need this in the niche case # that self.as_result() is Overrun but we still want the # examples for reporting purposes. self.__spans: Spans | None = None # We want the top level span to have depth 0, so we start at -1. self.depth: int = -1 self.__span_record = SpanRecord() # Slice indices for discrete reportable parts that which-parts-matter can # try varying, to report if the minimal example always fails anyway. self.arg_slices: set[tuple[int, int]] = set() self.slice_comments: dict[tuple[int, int], str] = {} self._observability_args: dict[str, Any] = {} self._observability_predicates: defaultdict[str, PredicateCounts] = defaultdict( PredicateCounts ) self._sampled_from_all_strategies_elements_message: ( tuple[str, object] | None ) = None self._shared_strategy_draws: dict[Hashable, tuple[Any, SearchStrategy]] = {} self._shared_data_strategy: DataObject | None = None self._stateful_repr_parts: list[Any] | None = None self.states_for_ids: dict[int, RandomState] | None = None self.seeds_to_states: dict[Any, RandomState] | None = None self.hypothesis_runner: Any = not_set self.expected_exception: BaseException | None = None self.expected_traceback: str | None = None self.prefix = prefix self.nodes: tuple[ChoiceNode, ...] = () self.misaligned_at: MisalignedAt | None = None self.cannot_proceed_scope: CannotProceedScopeT | None = None self.start_span(TOP_LABEL) def __repr__(self) -> str: return "ConjectureData(%s, %d choices%s)" % ( self.status.name, len(self.nodes), ", frozen" if self.frozen else "", ) @property def choices(self) -> tuple[ChoiceT, ...]: return tuple(node.value for node in self.nodes) # draw_* functions might be called in one of two contexts: either "above" or # "below" the choice sequence. For instance, draw_string calls draw_boolean # from ``many`` when calculating the number of characters to return. We do # not want these choices to get written to the choice sequence, because they # are not true choices themselves. # # `observe` formalizes this. The choice will only be written to the choice # sequence if observe is True. @overload def _draw( self, choice_type: Literal["integer"], constraints: IntegerConstraints, *, observe: bool, forced: int | None, ) -> int: ... @overload def _draw( self, choice_type: Literal["float"], constraints: FloatConstraints, *, observe: bool, forced: float | None, ) -> float: ... @overload def _draw( self, choice_type: Literal["string"], constraints: StringConstraints, *, observe: bool, forced: str | None, ) -> str: ... @overload def _draw( self, choice_type: Literal["bytes"], constraints: BytesConstraints, *, observe: bool, forced: bytes | None, ) -> bytes: ... @overload def _draw( self, choice_type: Literal["boolean"], constraints: BooleanConstraints, *, observe: bool, forced: bool | None, ) -> bool: ... def _draw( self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, observe: bool, forced: ChoiceT | None, ) -> ChoiceT: # this is somewhat redundant with the length > max_length check at the # end of the function, but avoids trying to use a null self.random when # drawing past the node of a ConjectureData.for_choices data. if self.length == self.max_length: debug_report(f"overrun because hit {self.max_length=}") self.mark_overrun() if len(self.nodes) == self.max_choices: debug_report(f"overrun because hit {self.max_choices=}") self.mark_overrun() if observe and self.prefix is not None and self.index < len(self.prefix): value = self._pop_choice(choice_type, constraints, forced=forced) elif forced is None: value = getattr(self.provider, f"draw_{choice_type}")(**constraints) if forced is not None: value = forced # nan values generated via int_to_float break list membership: # # >>> n = 18444492273895866368 # >>> assert math.isnan(int_to_float(n)) # >>> assert int_to_float(n) not in [int_to_float(n)] # # because int_to_float nans are not equal in the sense of either # `a == b` or `a is b`. # # This can lead to flaky errors when collections require unique # floats. What was happening is that in some places we provided math.nan # provide math.nan, and in others we provided # int_to_float(float_to_int(math.nan)), and which one gets used # was not deterministic across test iterations. # # To fix this, *never* provide a nan value which is equal (via `is`) to # another provided nan value. This sacrifices some test power; we should # bring that back (ABOVE the choice sequence layer) in the future. # # See https://github.com/HypothesisWorks/hypothesis/issues/3926. if choice_type == "float": assert isinstance(value, float) if math.isnan(value): value = int_to_float(float_to_int(value)) if observe: was_forced = forced is not None getattr(self.observer, f"draw_{choice_type}")( value, constraints=constraints, was_forced=was_forced ) size = 0 if self.provider.avoid_realization else choices_size([value]) if self.length + size > self.max_length: debug_report( f"overrun because {self.length=} + {size=} > {self.max_length=}" ) self.mark_overrun() node = ChoiceNode( type=choice_type, value=value, constraints=constraints, was_forced=was_forced, index=len(self.nodes), ) self.__span_record.record_choice() self.nodes += (node,) self.length += size return value def draw_integer( self, min_value: int | None = None, max_value: int | None = None, *, weights: dict[int, float] | None = None, shrink_towards: int = 0, forced: int | None = None, observe: bool = True, ) -> int: # Validate arguments if weights is not None: assert min_value is not None assert max_value is not None assert len(weights) <= 255 # arbitrary practical limit # We can and should eventually support total weights. But this # complicates shrinking as we can no longer assume we can force # a value to the unmapped probability mass if that mass might be 0. assert sum(weights.values()) < 1 # similarly, things get simpler if we assume every value is possible. # we'll want to drop this restriction eventually. assert all(w != 0 for w in weights.values()) if forced is not None and min_value is not None: assert min_value <= forced if forced is not None and max_value is not None: assert forced <= max_value constraints: IntegerConstraints = self._pooled_constraints( "integer", { "min_value": min_value, "max_value": max_value, "weights": weights, "shrink_towards": shrink_towards, }, ) return self._draw("integer", constraints, observe=observe, forced=forced) def draw_float( self, min_value: float = -math.inf, max_value: float = math.inf, *, allow_nan: bool = True, smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL, # TODO: consider supporting these float widths at the choice sequence # level in the future. # width: Literal[16, 32, 64] = 64, forced: float | None = None, observe: bool = True, ) -> float: assert smallest_nonzero_magnitude > 0 assert not math.isnan(min_value) assert not math.isnan(max_value) if smallest_nonzero_magnitude == 0.0: # pragma: no cover raise FloatingPointError( "Got allow_subnormal=True, but we can't represent subnormal floats " "right now, in violation of the IEEE-754 floating-point " "specification. This is usually because something was compiled with " "-ffast-math or a similar option, which sets global processor state. " "See https://simonbyrne.github.io/notes/fastmath/ for a more detailed " "writeup - and good luck!" ) if forced is not None: assert allow_nan or not math.isnan(forced) assert math.isnan(forced) or ( sign_aware_lte(min_value, forced) and sign_aware_lte(forced, max_value) ) constraints: FloatConstraints = self._pooled_constraints( "float", { "min_value": min_value, "max_value": max_value, "allow_nan": allow_nan, "smallest_nonzero_magnitude": smallest_nonzero_magnitude, }, ) return self._draw("float", constraints, observe=observe, forced=forced) def draw_string( self, intervals: IntervalSet, *, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, forced: str | None = None, observe: bool = True, ) -> str: assert forced is None or min_size <= len(forced) <= max_size assert min_size >= 0 if len(intervals) == 0: assert min_size == 0 constraints: StringConstraints = self._pooled_constraints( "string", { "intervals": intervals, "min_size": min_size, "max_size": max_size, }, ) return self._draw("string", constraints, observe=observe, forced=forced) def draw_bytes( self, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, *, forced: bytes | None = None, observe: bool = True, ) -> bytes: assert forced is None or min_size <= len(forced) <= max_size assert min_size >= 0 constraints: BytesConstraints = self._pooled_constraints( "bytes", {"min_size": min_size, "max_size": max_size} ) return self._draw("bytes", constraints, observe=observe, forced=forced) def draw_boolean( self, p: float = 0.5, *, forced: bool | None = None, observe: bool = True, ) -> bool: assert (forced is not True) or p > 0 assert (forced is not False) or p < 1 constraints: BooleanConstraints = self._pooled_constraints("boolean", {"p": p}) return self._draw("boolean", constraints, observe=observe, forced=forced) @overload def _pooled_constraints( self, choice_type: Literal["integer"], constraints: IntegerConstraints ) -> IntegerConstraints: ... @overload def _pooled_constraints( self, choice_type: Literal["float"], constraints: FloatConstraints ) -> FloatConstraints: ... @overload def _pooled_constraints( self, choice_type: Literal["string"], constraints: StringConstraints ) -> StringConstraints: ... @overload def _pooled_constraints( self, choice_type: Literal["bytes"], constraints: BytesConstraints ) -> BytesConstraints: ... @overload def _pooled_constraints( self, choice_type: Literal["boolean"], constraints: BooleanConstraints ) -> BooleanConstraints: ... def _pooled_constraints( self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT ) -> ChoiceConstraintsT: """Memoize common dictionary objects to reduce memory pressure.""" # caching runs afoul of nondeterminism checks if self.provider.avoid_realization: return constraints key = (choice_type, *choice_constraints_key(choice_type, constraints)) try: return POOLED_CONSTRAINTS_CACHE[key] except KeyError: POOLED_CONSTRAINTS_CACHE[key] = constraints return constraints def _pop_choice( self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, forced: ChoiceT | None, ) -> ChoiceT: assert self.prefix is not None # checked in _draw assert self.index < len(self.prefix) value = self.prefix[self.index] if isinstance(value, ChoiceTemplate): node: ChoiceTemplate = value if node.count is not None: assert node.count >= 0 # node templates have to be at the end for now, since it's not immediately # apparent how to handle overruning a node template while generating a single # node if the alternative is not "the entire data is an overrun". assert self.index == len(self.prefix) - 1 if node.type == "simplest": if forced is not None: choice = forced try: choice = choice_from_index(0, choice_type, constraints) except ChoiceTooLarge: self.mark_overrun() else: raise NotImplementedError if node.count is not None: node.count -= 1 if node.count < 0: self.mark_overrun() return choice choice = value node_choice_type = { str: "string", float: "float", int: "integer", bool: "boolean", bytes: "bytes", }[type(choice)] # If we're trying to: # * draw a different choice type at the same location # * draw the same choice type with a different constraints, which does not permit # the current value # # then we call this a misalignment, because the choice sequence has # changed from what we expected at some point. An easy misalignment is # # one_of(integers(0, 100), integers(101, 200)) # # where the choice sequence [0, 100] has constraints {min_value: 0, max_value: 100} # at index 1, but [0, 101] has constraints {min_value: 101, max_value: 200} at # index 1 (which does not permit any of the values 0-100). # # When the choice sequence becomes misaligned, we generate a new value of the # type and constraints the strategy expects. if node_choice_type != choice_type or not choice_permitted(choice, constraints): # only track first misalignment for now. if self.misaligned_at is None: self.misaligned_at = (self.index, choice_type, constraints, forced) try: # Fill in any misalignments with index 0 choices. An alternative to # this is using the index of the misaligned choice instead # of index 0, which may be useful for maintaining # "similarly-complex choices" in the shrinker. This requires # attaching an index to every choice in ConjectureData.for_choices, # which we don't always have (e.g. when reading from db). # # If we really wanted this in the future we could make this complexity # optional, use it if present, and default to index 0 otherwise. # This complicates our internal api and so I'd like to avoid it # if possible. # # Additionally, I don't think slips which require # slipping to high-complexity values are common. Though arguably # we may want to expand a bit beyond *just* the simplest choice. # (we could for example consider sampling choices from index 0-10). choice = choice_from_index(0, choice_type, constraints) except ChoiceTooLarge: # should really never happen with a 0-index choice, but let's be safe. self.mark_overrun() self.index += 1 return choice def as_result(self) -> ConjectureResult | _Overrun: """Convert the result of running this test into either an Overrun object or a ConjectureResult.""" assert self.frozen if self.status == Status.OVERRUN: return Overrun if self.__result is None: self.__result = ConjectureResult( status=self.status, interesting_origin=self.interesting_origin, spans=self.spans, nodes=self.nodes, length=self.length, output=self.output, expected_traceback=self.expected_traceback, expected_exception=self.expected_exception, has_discards=self.has_discards, target_observations=self.target_observations, tags=frozenset(self.tags), arg_slices=self.arg_slices, slice_comments=self.slice_comments, misaligned_at=self.misaligned_at, cannot_proceed_scope=self.cannot_proceed_scope, ) assert self.__result is not None return self.__result def __assert_not_frozen(self, name: str) -> None: if self.frozen: raise Frozen(f"Cannot call {name} on frozen ConjectureData") def note(self, value: Any) -> None: self.__assert_not_frozen("note") if not isinstance(value, str): value = repr(value) self.output += value def draw( self, strategy: "SearchStrategy[Ex]", label: int | None = None, observe_as: str | None = None, ) -> "Ex": from hypothesis.internal.observability import observability_enabled from hypothesis.strategies._internal.lazy import unwrap_strategies from hypothesis.strategies._internal.utils import to_jsonable at_top_level = self.depth == 0 start_time = None if at_top_level: # We start this timer early, because accessing attributes on a LazyStrategy # can be almost arbitrarily slow. In cases like characters() and text() # where we cache something expensive, this led to Flaky deadline errors! # See https://github.com/HypothesisWorks/hypothesis/issues/2108 start_time = time.perf_counter() gc_start_time = gc_cumulative_time() strategy.validate() if strategy.is_empty: self.mark_invalid(f"empty strategy {self!r}") if self.depth >= MAX_DEPTH: self.mark_invalid("max depth exceeded") # Jump directly to the unwrapped strategy for the label and for do_draw. # This avoids adding an extra span to all lazy strategies. unwrapped = unwrap_strategies(strategy) if label is None: label = unwrapped.label assert isinstance(label, int) self.start_span(label=label) try: if not at_top_level: return unwrapped.do_draw(self) assert start_time is not None key = observe_as or f"generate:unlabeled_{len(self.draw_times)}" try: try: v = unwrapped.do_draw(self) finally: # Subtract the time spent in GC to avoid overcounting, as it is # accounted for at the overall example level. in_gctime = gc_cumulative_time() - gc_start_time self.draw_times[key] = time.perf_counter() - start_time - in_gctime except Exception as err: add_note( err, f"while generating {key.removeprefix('generate:')!r} from {strategy!r}", ) raise if observability_enabled(): avoid = self.provider.avoid_realization self._observability_args[key] = to_jsonable(v, avoid_realization=avoid) return v finally: self.stop_span() def start_span(self, label: int) -> None: self.provider.span_start(label) self.__assert_not_frozen("start_span") self.depth += 1 # Logically it would make sense for this to just be # ``self.depth = max(self.depth, self.max_depth)``, which is what it used to # be until we ran the code under tracemalloc and found a rather significant # chunk of allocation was happening here. This was presumably due to varargs # or the like, but we didn't investigate further given that it was easy # to fix with this check. if self.depth > self.max_depth: self.max_depth = self.depth self.__span_record.start_span(label) self.labels_for_structure_stack.append({label}) def stop_span(self, *, discard: bool = False) -> None: self.provider.span_end(discard) if self.frozen: return if discard: self.has_discards = True self.depth -= 1 assert self.depth >= -1 self.__span_record.stop_span(discard=discard) labels_for_structure = self.labels_for_structure_stack.pop() if not discard: if self.labels_for_structure_stack: self.labels_for_structure_stack[-1].update(labels_for_structure) else: self.tags.update([structural_coverage(l) for l in labels_for_structure]) if discard: # Once we've discarded a span, every test case starting with # this prefix contains discards. We prune the tree at that point so # as to avoid future test cases bothering with this region, on the # assumption that some span that you could have used instead # there would *not* trigger the discard. This greatly speeds up # test case generation in some cases, because it allows us to # ignore large swathes of the search space that are effectively # redundant. # # A scenario that can cause us problems but which we deliberately # have decided not to support is that if there are side effects # during data generation then you may end up with a scenario where # every good test case generates a discard because the discarded # section sets up important things for later. This is not terribly # likely and all that you see in this case is some degradation in # quality of testing, so we don't worry about it. # # Note that killing the branch does *not* mean we will never # explore below this point, and in particular we may do so during # shrinking. Any explicit request for a data object that starts # with the branch here will work just fine, but novel prefix # generation will avoid it, and we can use it to detect when we # have explored the entire tree (up to redundancy). self.observer.kill_branch() @property def spans(self) -> Spans: assert self.frozen if self.__spans is None: self.__spans = Spans(record=self.__span_record) return self.__spans def freeze(self) -> None: if self.frozen: return self.finish_time = time.perf_counter() self.gc_finish_time = gc_cumulative_time() # Always finish by closing all remaining spans so that we have a valid tree. while self.depth >= 0: self.stop_span() self.__span_record.freeze() self.frozen = True self.observer.conclude_test(self.status, self.interesting_origin) def choice( self, values: Sequence[T], *, forced: T | None = None, observe: bool = True, ) -> T: forced_i = None if forced is None else values.index(forced) i = self.draw_integer( 0, len(values) - 1, forced=forced_i, observe=observe, ) return values[i] def conclude_test( self, status: Status, interesting_origin: InterestingOrigin | None = None, ) -> NoReturn: assert (interesting_origin is None) or (status == Status.INTERESTING) self.__assert_not_frozen("conclude_test") self.interesting_origin = interesting_origin self.status = status self.freeze() raise StopTest(self.testcounter) def mark_interesting(self, interesting_origin: InterestingOrigin) -> NoReturn: self.conclude_test(Status.INTERESTING, interesting_origin) def mark_invalid(self, why: str | None = None) -> NoReturn: if why is not None: self.events["invalid because"] = why self.conclude_test(Status.INVALID) def mark_overrun(self) -> NoReturn: self.conclude_test(Status.OVERRUN) def draw_choice( choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, random: Random ) -> ChoiceT: cd = ConjectureData(random=random) return cast(ChoiceT, getattr(cd.provider, f"draw_{choice_type}")(**constraints)) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/datatree.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from collections.abc import Generator, Set from dataclasses import dataclass, field from random import Random from typing import TYPE_CHECKING, Final, TypeAlias, cast from hypothesis.errors import ( FlakyReplay, FlakyStrategyDefinition, HypothesisException, StopTest, ) from hypothesis.internal import floats as flt from hypothesis.internal.conjecture.choice import ( BooleanConstraints, BytesConstraints, ChoiceConstraintsT, ChoiceT, ChoiceTypeT, FloatConstraints, IntegerConstraints, StringConstraints, choice_from_index, ) from hypothesis.internal.conjecture.data import ConjectureData, DataObserver, Status from hypothesis.internal.escalation import InterestingOrigin from hypothesis.internal.floats import ( count_between_floats, float_to_int, int_to_float, sign_aware_lte, ) if TYPE_CHECKING: from hypothesis.vendor.pretty import RepresentationPrinter ChildrenCacheValueT: TypeAlias = tuple[ Generator[ChoiceT, None, None], list[ChoiceT], set[ChoiceT] ] class PreviouslyUnseenBehaviour(HypothesisException): pass _FLAKY_STRAT_MSG = ( "Inconsistent data generation! Data generation behaved differently " "between different runs. Is your data generation depending on external " "state?" ) EMPTY: frozenset[int] = frozenset() @dataclass(slots=True, frozen=True) class Killed: """Represents a transition to part of the tree which has been marked as "killed", meaning we want to treat it as not worth exploring, so it will be treated as if it were completely explored for the purposes of exhaustion.""" next_node: "TreeNode" def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: assert cycle is False p.text("Killed") def _node_pretty( choice_type: ChoiceTypeT, value: ChoiceT, constraints: ChoiceConstraintsT, *, forced: bool, ) -> str: forced_marker = " [forced]" if forced else "" return f"{choice_type} {value!r}{forced_marker} {constraints}" @dataclass(slots=True, frozen=False) class Branch: """Represents a transition where multiple choices can be made as to what to drawn.""" constraints: ChoiceConstraintsT choice_type: ChoiceTypeT children: dict[ChoiceT, "TreeNode"] = field(repr=False) @property def max_children(self) -> int: max_children = compute_max_children(self.choice_type, self.constraints) assert max_children > 0 return max_children def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: assert cycle is False for i, (value, child) in enumerate(self.children.items()): if i > 0: p.break_() p.text( _node_pretty(self.choice_type, value, self.constraints, forced=False) ) with p.indent(2): p.break_() p.pretty(child) @dataclass(slots=True, frozen=True) class Conclusion: """Represents a transition to a finished state.""" status: Status interesting_origin: InterestingOrigin | None def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: assert cycle is False o = self.interesting_origin # avoid str(o), which can include multiple lines of context origin = ( "" if o is None else f", {o.exc_type.__name__} at {o.filename}:{o.lineno}" ) p.text(f"Conclusion ({self.status!r}{origin})") # The number of max children where, beyond this, it is practically impossible # for hypothesis to saturate / explore all children nodes in a reasonable time # frame. We use this to bail out of expensive max children computations early, # where the numbers involved are so large that we know they will be larger than # this number. # # Note that it's ok for us to underestimate the number of max children of a node # by using this. We just may think the node is exhausted when in fact it has more # possible children to be explored. This has the potential to finish generation # early due to exhausting the entire tree, but that is quite unlikely: (1) the # number of examples would have to be quite high, and (2) the tree would have to # contain only one or two nodes, or generate_novel_prefix would simply switch to # exploring another non-exhausted node. # # Also note that we may sometimes compute max children above this value. In other # words, this is *not* a hard maximum on the computed max children. It's the point # where further computation is not beneficial - but sometimes doing that computation # unconditionally is cheaper than estimating against this value. # # The one case where this may be detrimental is fuzzing, where the throughput of # examples is so high that it really may saturate important nodes. We'll cross # that bridge when we come to it. MAX_CHILDREN_EFFECTIVELY_INFINITE: Final[int] = 10_000_000 def _count_distinct_strings(*, alphabet_size: int, min_size: int, max_size: int) -> int: # We want to estimate if we're going to have more children than # MAX_CHILDREN_EFFECTIVELY_INFINITE, without computing a potentially # extremely expensive pow. We'll check the two extreme cases - if the # number of strings in the largest string size alone is enough to put us # over this limit (at alphabet_size >= 2), and if the variation in sizes # (at alphabet_size == 1) is enough. If neither result in an early return, # the exact result should be reasonably cheap to compute. if alphabet_size == 0: # Special-case the empty string, avoid error in math.log(0). return 1 elif alphabet_size == 1: # Special-case the constant alphabet, invalid in the geom-series sum. return max_size - min_size + 1 else: # Estimate against log, which is cheaper than computing a pow. # # m = max_size # a = alphabet_size # N = MAX_CHILDREN_EFFECTIVELY_INFINITE # # a**m > N # <=> m * log(a) > log(N) log_max_sized_children = max_size * math.log(alphabet_size) if log_max_sized_children > math.log(MAX_CHILDREN_EFFECTIVELY_INFINITE): return MAX_CHILDREN_EFFECTIVELY_INFINITE # The sum of a geometric series is given by (ref: wikipedia): # ᵐ∑ₖ₌₀ aᵏ = (aᵐ⁺¹ - 1) / (a - 1) # = S(m) / S(0) # assuming a != 1 and using the definition # S(m) := aᵐ⁺¹ - 1. # The sum we want, starting from a number n [0 <= n <= m] rather than zero, is # ᵐ∑ₖ₌ₙ aᵏ = ᵐ∑ₖ₌₀ aᵏ - ⁿ⁻¹∑ₖ₌₀ aᵏ = S(m) / S(0) - S(n - 1) / S(0) def S(n): return alphabet_size ** (n + 1) - 1 return (S(max_size) - S(min_size - 1)) // S(0) def compute_max_children( choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT ) -> int: if choice_type == "integer": constraints = cast(IntegerConstraints, constraints) min_value = constraints["min_value"] max_value = constraints["max_value"] if min_value is None and max_value is None: # full 128 bit range. return 2**128 - 1 if min_value is not None and max_value is not None: # count between min/max value. return max_value - min_value + 1 # hard case: only one bound was specified. Here we probe either upwards # or downwards with our full 128 bit generation, but only half of these # (plus one for the case of generating zero) result in a probe in the # direction we want. ((2**128 - 1) // 2) + 1 == 2 ** 127 assert (min_value is None) != (max_value is None) return 2**127 elif choice_type == "boolean": constraints = cast(BooleanConstraints, constraints) p = constraints["p"] # probabilities of 0 or 1 (or effectively 0 or 1) only have one choice. if p <= 2 ** (-64) or p >= (1 - 2 ** (-64)): return 1 return 2 elif choice_type == "bytes": constraints = cast(BytesConstraints, constraints) return _count_distinct_strings( alphabet_size=2**8, min_size=constraints["min_size"], max_size=constraints["max_size"], ) elif choice_type == "string": constraints = cast(StringConstraints, constraints) min_size = constraints["min_size"] max_size = constraints["max_size"] intervals = constraints["intervals"] return _count_distinct_strings( alphabet_size=len(intervals), min_size=min_size, max_size=max_size ) elif choice_type == "float": constraints = cast(FloatConstraints, constraints) min_value_f = constraints["min_value"] max_value_f = constraints["max_value"] smallest_nonzero_magnitude = constraints["smallest_nonzero_magnitude"] count = count_between_floats(min_value_f, max_value_f) # we have two intervals: # a. [min_value, max_value] # b. [-smallest_nonzero_magnitude, smallest_nonzero_magnitude] # # which could be subsets (in either order), overlapping, or disjoint. We # want the interval difference a - b. # next_down because endpoints are ok with smallest_nonzero_magnitude min_point = max(min_value_f, -flt.next_down(smallest_nonzero_magnitude)) max_point = min(max_value_f, flt.next_down(smallest_nonzero_magnitude)) if min_point > max_point: # case: disjoint intervals. return count count -= count_between_floats(min_point, max_point) if sign_aware_lte(min_value_f, -0.0) and sign_aware_lte(-0.0, max_value_f): # account for -0.0 count += 1 if sign_aware_lte(min_value_f, 0.0) and sign_aware_lte(0.0, max_value_f): # account for 0.0 count += 1 return count raise NotImplementedError(f"unhandled choice_type {choice_type}") # In theory, this is a strict superset of the functionality of compute_max_children; # # assert len(all_children(choice_type, constraints)) == compute_max_children(choice_type, constraints) # # In practice, we maintain two distinct implementations for efficiency and space # reasons. If you just need the number of children, it is cheaper to use # compute_max_children than to reify the list of children (only to immediately # throw it away). def _floats_between(a: float, b: float) -> Generator[float, None, None]: for n in range(float_to_int(a), float_to_int(b) + 1): yield int_to_float(n) def all_children( choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT ) -> Generator[ChoiceT, None, None]: if choice_type != "float": for index in range(compute_max_children(choice_type, constraints)): yield choice_from_index(index, choice_type, constraints) else: constraints = cast(FloatConstraints, constraints) # the float ordering is not injective (because of resampling # out-of-bounds values), so using choice_from_index would result in # duplicates. This violates invariants in datatree about being able # to draw unique new children using all_children. # # We instead maintain a separate implementation for floats. # TODO_IR write a better (bijective) ordering for floats and remove this! min_value = constraints["min_value"] max_value = constraints["max_value"] smallest_nonzero_magnitude = constraints["smallest_nonzero_magnitude"] # handle zeroes separately so smallest_nonzero_magnitude can think of # itself as a complete interval (instead of a hole at ±0). if sign_aware_lte(min_value, -0.0) and sign_aware_lte(-0.0, max_value): yield -0.0 if sign_aware_lte(min_value, 0.0) and sign_aware_lte(0.0, max_value): yield 0.0 if flt.is_negative(min_value): if flt.is_negative(max_value): # case: both negative. max_point = min(max_value, -smallest_nonzero_magnitude) # float_to_int increases as negative magnitude increases, so # invert order. yield from _floats_between(max_point, min_value) else: # case: straddles midpoint (which is between -0.0 and 0.0). yield from _floats_between(-smallest_nonzero_magnitude, min_value) yield from _floats_between(smallest_nonzero_magnitude, max_value) else: # case: both positive. min_point = max(min_value, smallest_nonzero_magnitude) yield from _floats_between(min_point, max_value) @dataclass(slots=True, frozen=False) class TreeNode: """ A node, or collection of directly descended nodes, in a DataTree. We store the DataTree as a radix tree (https://en.wikipedia.org/wiki/Radix_tree), which means that nodes that are the only child of their parent are collapsed into their parent to save space. Conceptually, you can unfold a single TreeNode storing n values in its lists into a sequence of n nodes, each a child of the last. In other words, (constraints[i], values[i], choice_types[i]) corresponds to the single node at index i. Note that if a TreeNode represents a choice (i.e. the nodes cannot be compacted via the radix tree definition), then its lists will be empty and it will store a `Branch` representing that choce in its `transition`. Examples -------- Consider sequentially drawing a boolean, then an integer. data.draw_boolean() data.draw_integer(1, 3) If we draw True and then 2, the tree may conceptually look like this. ┌──────┐ │ root │ └──┬───┘ ┌──┴───┐ │ True │ └──┬───┘ ┌──┴───┐ │ 2 │ └──────┘ But since 2 is the only child of True, we will compact these nodes and store them as a single TreeNode. ┌──────┐ │ root │ └──┬───┘ ┌────┴──────┐ │ [True, 2] │ └───────────┘ If we then draw True and then 3, True will have multiple children and we can no longer store this compacted representation. We would call split_at(0) on the [True, 2] node to indicate that we need to add a choice at 0-index node (True). ┌──────┐ │ root │ └──┬───┘ ┌──┴───┐ ┌─┤ True ├─┐ │ └──────┘ │ ┌─┴─┐ ┌─┴─┐ │ 2 │ │ 3 │ └───┘ └───┘ """ # The constraints, value, and choice_types of the nodes stored here. These always # have the same length. The values at index i belong to node i. constraints: list[ChoiceConstraintsT] = field(default_factory=list) values: list[ChoiceT] = field(default_factory=list) choice_types: list[ChoiceTypeT] = field(default_factory=list) # The indices of nodes which had forced values. # # Stored as None if no indices have been forced, purely for space saving # reasons (we force quite rarely). __forced: set[int] | None = field(default=None, init=False) # What happens next after drawing these nodes. (conceptually, "what is the # child/children of the last node stored here"). # # One of: # - None (we don't know yet) # - Branch (we have seen multiple possible outcomes here) # - Conclusion (ConjectureData.conclude_test was called here) # - Killed (this branch is valid and may even have children, but should not # be explored when generating novel prefixes) transition: None | Branch | Conclusion | Killed = None # A tree node is exhausted if every possible sequence of draws below it has # been explored. We only update this when performing operations that could # change the answer. # # See also TreeNode.check_exhausted. is_exhausted: bool = field(default=False, init=False) @property def forced(self) -> Set[int]: if not self.__forced: return EMPTY return self.__forced def mark_forced(self, i: int) -> None: """ Note that the draw at node i was forced. """ assert 0 <= i < len(self.values) if self.__forced is None: self.__forced = set() self.__forced.add(i) def split_at(self, i: int) -> None: """ Splits the tree so that it can incorporate a decision at the draw call corresponding to the node at position i. Raises FlakyStrategyDefinition if node i was forced. """ if i in self.forced: raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) assert not self.is_exhausted key = self.values[i] child = TreeNode( choice_types=self.choice_types[i + 1 :], constraints=self.constraints[i + 1 :], values=self.values[i + 1 :], transition=self.transition, ) self.transition = Branch( constraints=self.constraints[i], choice_type=self.choice_types[i], children={key: child}, ) if self.__forced is not None: child.__forced = {j - i - 1 for j in self.__forced if j > i} self.__forced = {j for j in self.__forced if j < i} child.check_exhausted() del self.choice_types[i:] del self.values[i:] del self.constraints[i:] assert len(self.values) == len(self.constraints) == len(self.choice_types) == i def check_exhausted(self) -> bool: """ Recalculates is_exhausted if necessary, and then returns it. A node is exhausted if: - Its transition is Conclusion or Killed - It has the maximum number of children (i.e. we have found all of its possible children), and all its children are exhausted Therefore, we only need to compute this for a node when: - We first create it in split_at - We set its transition to either Conclusion or Killed (TreeRecordingObserver.conclude_test or TreeRecordingObserver.kill_branch) - We exhaust any of its children """ if ( # a node cannot go from is_exhausted -> not is_exhausted. not self.is_exhausted # if we don't know what happens after this node, we don't have # enough information to tell if it's exhausted. and self.transition is not None # if there are still any nodes left which are the only child of their # parent (len(self.values) > 0), then this TreeNode must be not # exhausted, unless all of those nodes were forced. # # This is because we maintain an invariant of only adding nodes to # DataTree which have at least 2 possible values, so we know that if # they do not have any siblings that we still have more choices to # discover. # # (We actually *do* currently add single-valued nodes to the tree, # but immediately split them into a transition to avoid falsifying # this check. this is a bit of a hack.) and len(self.forced) == len(self.values) ): if isinstance(self.transition, (Conclusion, Killed)): self.is_exhausted = True elif len(self.transition.children) == self.transition.max_children: self.is_exhausted = all( v.is_exhausted for v in self.transition.children.values() ) return self.is_exhausted def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: assert cycle is False indent = 0 for i, (choice_type, constraints, value) in enumerate( zip(self.choice_types, self.constraints, self.values, strict=True) ): with p.indent(indent): if i > 0: p.break_() p.text( _node_pretty( choice_type, value, constraints, forced=i in self.forced ) ) indent += 2 with p.indent(indent): if len(self.values) > 0: p.break_() if self.transition is not None: p.pretty(self.transition) else: p.text("unknown") class DataTree: """ A DataTree tracks the structured history of draws in some test function, across multiple ConjectureData objects. This information is used by ConjectureRunner to generate novel prefixes of this tree (see generate_novel_prefix). A novel prefix is a sequence of draws which the tree has not seen before, and therefore the ConjectureRunner has not generated as an input to the test function before. DataTree tracks the following: - Drawn choices in the choice sequence - ConjectureData.draw_integer() - ConjectureData.draw_float() - ConjectureData.draw_string() - ConjectureData.draw_boolean() - ConjectureData.draw_bytes() - Test conclusions (with some Status, e.g. Status.VALID) - ConjectureData.conclude_test() A DataTree is — surprise — a *tree*. A node in this tree is either a choice draw with some value, a test conclusion with some Status, or a special `Killed` value, which denotes that further draws may exist beyond this node but should not be considered worth exploring when generating novel prefixes. A node is a leaf iff it is a conclusion or Killed. A branch from node A to node B indicates that we have previously seen some sequence (a, b) of draws, where a and b are the values in nodes A and B. Similar intuition holds for conclusion and Killed nodes. Examples -------- To see how a DataTree gets built through successive sets of draws, consider the following code that calls through to some ConjecutreData object `data`. The first call can be either True or False, and the second call can be any integer in the range [1, 3]. data.draw_boolean() data.draw_integer(1, 3) To start, the corresponding DataTree object is completely empty. ┌──────┐ │ root │ └──────┘ We happen to draw True and then 2 in the above code. The tree tracks this. (2 also connects to a child Conclusion node with Status.VALID since it's the final draw in the code. I'll omit Conclusion nodes in diagrams for brevity.) ┌──────┐ │ root │ └──┬───┘ ┌──┴───┐ │ True │ └──┬───┘ ┌──┴───┐ │ 2 │ └──────┘ This is a very boring tree so far! But now we happen to draw False and then 1. This causes a split in the tree. Remember, DataTree tracks history over all invocations of a function, not just one. The end goal is to know what invocations haven't been tried yet, after all. ┌──────┐ ┌───┤ root ├───┐ │ └──────┘ │ ┌──┴───┐ ┌─┴─────┐ │ True │ │ False │ └──┬───┘ └──┬────┘ ┌─┴─┐ ┌─┴─┐ │ 2 │ │ 1 │ └───┘ └───┘ If we were to ask DataTree for a novel prefix at this point, it might generate any of (True, 1), (True, 3), (False, 2), or (False, 3). Note that the novel prefix stops as soon as it generates a novel node. For instance, if we had generated a novel prefix back when the tree was only root -> True -> 2, we could have gotten any of (True, 1), (True, 3), or (False). But we could *not* have gotten (False, n), because both False and n were novel at that point, and we stop at the first novel node — False. I won't belabor this example. Here's what the tree looks like when fully explored: ┌──────┐ ┌──────┤ root ├──────┐ │ └──────┘ │ ┌──┴───┐ ┌─┴─────┐ ┌──┤ True ├──┐ ┌───┤ False ├──┐ │ └──┬───┘ │ │ └──┬────┘ │ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ │ 1 │ │ 2 │ │ 3 │ │ 1 │ │ 2 │ │ 3 │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ You could imagine much more complicated trees than this arising in practice, and indeed they do. In particular, the tree need not be balanced or 'nice' like the tree above. For instance, b = data.draw_boolean() if b: data.draw_integer(1, 3) results in a tree with the entire right part lopped off, and False leading straight to a conclusion node with Status.VALID. As another example, n = data.draw_integers() assume(n >= 3) data.draw_string() results in a tree with the 0, 1, and 2 nodes leading straight to a conclusion node with Status.INVALID, and the rest branching off into all the possibilities of draw_string. Notes ----- The above examples are slightly simplified and are intended to convey intuition. In practice, there are some implementation details to be aware of. - In draw nodes, we store the constraints used in addition to the value drawn. E.g. the node corresponding to data.draw_float(min_value=1.0, max_value=1.5) would store {"min_value": 1.0, "max_value": 1.5, ...} (default values for other constraints omitted). The constraints parameters have the potential to change both the range of possible outputs of a node, and the probability distribution within that range, so we need to use these when drawing in DataTree as well. We draw values using these constraints when (1) generating a novel value for a node and (2) choosing a random child when traversing the tree. - For space efficiency, rather than tracking the full tree structure, we store DataTree as a radix tree. This is conceptually equivalent (radix trees can always be "unfolded" to the full tree) but it means the internal representation may differ in practice. See TreeNode for more information. """ def __init__(self) -> None: self.root: TreeNode = TreeNode() self._children_cache: dict[ChoiceT, ChildrenCacheValueT] = {} @property def is_exhausted(self) -> bool: """ Returns True if every node is exhausted, and therefore the tree has been fully explored. """ return self.root.is_exhausted def generate_novel_prefix(self, random: Random) -> tuple[ChoiceT, ...]: """Generate a short random string that (after rewriting) is not a prefix of any choice sequence previously added to the tree. The resulting prefix is essentially arbitrary - it would be nice for it to be uniform at random, but previous attempts to do that have proven too expensive. """ assert not self.is_exhausted prefix = [] def append_choice(choice_type: ChoiceTypeT, choice: ChoiceT) -> None: if choice_type == "float": assert isinstance(choice, int) choice = int_to_float(choice) prefix.append(choice) current_node = self.root while True: assert not current_node.is_exhausted for i, (choice_type, constraints, value) in enumerate( zip( current_node.choice_types, current_node.constraints, current_node.values, strict=True, ) ): if i in current_node.forced: append_choice(choice_type, value) else: attempts = 0 while True: if attempts <= 10: try: node_value = self._draw( choice_type, constraints, random=random ) except StopTest: # pragma: no cover # it is possible that drawing from a fresh data can # overrun BUFFER_SIZE, due to eg unlucky rejection sampling # of integer probes. Retry these cases. attempts += 1 continue else: node_value = self._draw_from_cache( choice_type, constraints, key=id(current_node), random=random, ) if node_value != value: append_choice(choice_type, node_value) break attempts += 1 self._reject_child( choice_type, constraints, child=node_value, key=id(current_node), ) # We've now found a value that is allowed to # vary, so what follows is not fixed. return tuple(prefix) assert not isinstance(current_node.transition, (Conclusion, Killed)) if current_node.transition is None: return tuple(prefix) branch = current_node.transition assert isinstance(branch, Branch) attempts = 0 while True: if attempts <= 10: try: node_value = self._draw( branch.choice_type, branch.constraints, random=random ) except StopTest: # pragma: no cover attempts += 1 continue else: node_value = self._draw_from_cache( branch.choice_type, branch.constraints, key=id(branch), random=random, ) try: child = branch.children[node_value] except KeyError: append_choice(branch.choice_type, node_value) return tuple(prefix) if not child.is_exhausted: append_choice(branch.choice_type, node_value) current_node = child break attempts += 1 self._reject_child( branch.choice_type, branch.constraints, child=node_value, key=id(branch), ) # We don't expect this assertion to ever fire, but coverage # wants the loop inside to run if you have branch checking # on, hence the pragma. assert ( # pragma: no cover attempts != 1000 or len(branch.children) < branch.max_children or any(not v.is_exhausted for v in branch.children.values()) ) def rewrite(self, choices): """Use previously seen ConjectureData objects to return a tuple of the rewritten choice sequence and the status we would get from running that with the test function. If the status cannot be predicted from the existing values it will be None.""" data = ConjectureData.for_choices(choices) try: self.simulate_test_function(data) return (data.choices, data.status) except PreviouslyUnseenBehaviour: return (choices, None) def simulate_test_function(self, data: ConjectureData) -> None: """Run a simulated version of the test function recorded by this tree. Note that this does not currently call ``stop_span`` or ``start_span`` as these are not currently recorded in the tree. This will likely change in future.""" node = self.root def draw(choice_type, constraints, *, forced=None, convert_forced=True): if choice_type == "float" and forced is not None and convert_forced: forced = int_to_float(forced) draw_func = getattr(data, f"draw_{choice_type}") value = draw_func(**constraints, forced=forced) if choice_type == "float": value = float_to_int(value) return value try: while True: for i, (choice_type, constraints, previous) in enumerate( zip(node.choice_types, node.constraints, node.values, strict=True) ): v = draw( choice_type, constraints, forced=previous if i in node.forced else None, ) if v != previous: raise PreviouslyUnseenBehaviour if isinstance(node.transition, Conclusion): t = node.transition data.conclude_test(t.status, t.interesting_origin) elif node.transition is None: raise PreviouslyUnseenBehaviour elif isinstance(node.transition, Branch): v = draw(node.transition.choice_type, node.transition.constraints) try: node = node.transition.children[v] except KeyError as err: raise PreviouslyUnseenBehaviour from err else: assert isinstance(node.transition, Killed) data.observer.kill_branch() node = node.transition.next_node except StopTest: pass def new_observer(self): return TreeRecordingObserver(self) def _draw( self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, random: Random, ) -> ChoiceT: from hypothesis.internal.conjecture.data import draw_choice value = draw_choice(choice_type, constraints, random=random) # using floats as keys into branch.children breaks things, because # e.g. hash(0.0) == hash(-0.0) would collide as keys when they are # in fact distinct child branches. # To distinguish floats here we'll use their bits representation. This # entails some bookkeeping such that we're careful about when the # float key is in its bits form (as a key into branch.children) and # when it is in its float form (as a value we want to write to the # choice sequence), and converting between the two forms as appropriate. if choice_type == "float": assert isinstance(value, float) value = float_to_int(value) return value def _get_children_cache( self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, key: ChoiceT ) -> ChildrenCacheValueT: # cache the state of the children generator per node/branch (passed as # `key` here), such that we track which children we've already tried # for this branch across draws. # We take advantage of python generators here as one-way iterables, # so each time we iterate we implicitly store our position in the # children generator and don't re-draw children. `children` is the # concrete list of children draw from the generator that we will work # with. Whenever we need to top up this list, we will draw a new value # from the generator. if key not in self._children_cache: generator = all_children(choice_type, constraints) children: list[ChoiceT] = [] rejected: set[ChoiceT] = set() self._children_cache[key] = (generator, children, rejected) return self._children_cache[key] def _draw_from_cache( self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, key: ChoiceT, random: Random, ) -> ChoiceT: (generator, children, rejected) = self._get_children_cache( choice_type, constraints, key=key ) # Keep a stock of 100 potentially-valid children at all times. # This number is chosen to balance memory/speed vs randomness. Ideally # we would sample uniformly from all not-yet-rejected children, but # computing and storing said children is not free. # no-branch because coverage of the fall-through case here is a bit # annoying. if len(children) < 100: # pragma: no branch for v in generator: if choice_type == "float": assert isinstance(v, float) v = float_to_int(v) if v in rejected: continue children.append(v) if len(children) >= 100: break return random.choice(children) def _reject_child( self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, child: ChoiceT, key: ChoiceT, ) -> None: (_generator, children, rejected) = self._get_children_cache( choice_type, constraints, key=key ) rejected.add(child) # we remove a child from the list of possible children *only* when it is # rejected, and not when it is initially drawn in _draw_from_cache. The # reason is that a child being drawn does not guarantee that child will # be used in a way such that it is written back to the tree, so it needs # to be available for future draws until we are certain it has been # used. # # For instance, if we generated novel prefixes in a loop (but never used # those prefixes to generate new values!) then we don't want to remove # the drawn children from the available pool until they are actually # used. # # This does result in a small inefficiency: we may draw a child, # immediately use it (so we know it cannot be drawn again), but still # wait to draw and reject it here, because DataTree cannot guarantee # the drawn child has been used. if child in children: children.remove(child) def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: assert cycle is False p.pretty(self.root) class TreeRecordingObserver(DataObserver): def __init__(self, tree: DataTree): # this attr isn't read, but is very useful for local debugging flaky # errors, with # `from hypothesis.vendor import pretty; print(pretty.pretty(self._root))` self._root = tree.root self._current_node: TreeNode = tree.root self._index_in_current_node: int = 0 self._trail: list[TreeNode] = [self._current_node] self.killed: bool = False def draw_integer( self, value: int, *, was_forced: bool, constraints: IntegerConstraints ) -> None: self.draw_value( "integer", value, was_forced=was_forced, constraints=constraints ) def draw_float( self, value: float, *, was_forced: bool, constraints: FloatConstraints ) -> None: self.draw_value("float", value, was_forced=was_forced, constraints=constraints) def draw_string( self, value: str, *, was_forced: bool, constraints: StringConstraints ) -> None: self.draw_value("string", value, was_forced=was_forced, constraints=constraints) def draw_bytes( self, value: bytes, *, was_forced: bool, constraints: BytesConstraints ) -> None: self.draw_value("bytes", value, was_forced=was_forced, constraints=constraints) def draw_boolean( self, value: bool, *, was_forced: bool, constraints: BooleanConstraints ) -> None: self.draw_value( "boolean", value, was_forced=was_forced, constraints=constraints ) def draw_value( self, choice_type: ChoiceTypeT, value: ChoiceT, *, was_forced: bool, constraints: ChoiceConstraintsT, ) -> None: i = self._index_in_current_node self._index_in_current_node += 1 node = self._current_node if isinstance(value, float): value = float_to_int(value) assert len(node.constraints) == len(node.values) == len(node.choice_types) if i < len(node.values): if ( choice_type != node.choice_types[i] or constraints != node.constraints[i] ): raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) # Note that we don't check whether a previously # forced value is now free. That will be caught # if we ever split the node there, but otherwise # may pass silently. This is acceptable because it # means we skip a hash set lookup on every # draw and that's a pretty niche failure mode. if was_forced and i not in node.forced: raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) if value != node.values[i]: node.split_at(i) assert i == len(node.values) new_node = TreeNode() assert isinstance(node.transition, Branch) node.transition.children[value] = new_node self._current_node = new_node self._index_in_current_node = 0 else: trans = node.transition if trans is None: node.choice_types.append(choice_type) node.constraints.append(constraints) node.values.append(value) if was_forced: node.mark_forced(i) # generate_novel_prefix assumes the following invariant: any one # of the series of draws in a particular node can vary, i.e. the # max number of children is at least 2. However, some draws are # pseudo-choices and only have a single value, such as # integers(0, 0). # # Currently, we address this by forcefully splitting such # single-valued nodes into a transition when we see them. An # exception to this is if it was forced: forced pseudo-choices # do not cause the above issue because they inherently cannot # vary, and moreover they trip other invariants about never # splitting forced nodes. # # An alternative is not writing such choices to the tree at # all, and thus guaranteeing that each node has at least 2 max # children. if ( compute_max_children(choice_type, constraints) == 1 and not was_forced ): node.split_at(i) assert isinstance(node.transition, Branch) self._current_node = node.transition.children[value] self._index_in_current_node = 0 elif isinstance(trans, Conclusion): assert trans.status != Status.OVERRUN # We tried to draw where history says we should have # stopped raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) else: assert isinstance(trans, Branch), trans if choice_type != trans.choice_type or constraints != trans.constraints: raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) try: self._current_node = trans.children[value] except KeyError: self._current_node = trans.children.setdefault(value, TreeNode()) self._index_in_current_node = 0 if self._trail[-1] is not self._current_node: self._trail.append(self._current_node) def kill_branch(self) -> None: """Mark this part of the tree as not worth re-exploring.""" if self.killed: return self.killed = True if self._index_in_current_node < len(self._current_node.values) or ( self._current_node.transition is not None and not isinstance(self._current_node.transition, Killed) ): raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) if self._current_node.transition is None: self._current_node.transition = Killed(TreeNode()) self.__update_exhausted() self._current_node = self._current_node.transition.next_node self._index_in_current_node = 0 self._trail.append(self._current_node) def conclude_test( self, status: Status, interesting_origin: InterestingOrigin | None ) -> None: """Says that ``status`` occurred at node ``node``. This updates the node if necessary and checks for consistency.""" if status == Status.OVERRUN: return i = self._index_in_current_node node = self._current_node if i < len(node.values) or isinstance(node.transition, Branch): raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) new_transition = Conclusion(status, interesting_origin) if node.transition is not None and node.transition != new_transition: # As an, I'm afraid, horrible bodge, we deliberately ignore flakiness # where tests go from interesting to valid, because it's much easier # to produce good error messages for these further up the stack. if isinstance(node.transition, Conclusion) and ( node.transition.status != Status.INTERESTING or new_transition.status != Status.VALID ): old_origin = node.transition.interesting_origin new_origin = new_transition.interesting_origin raise FlakyReplay( f"Inconsistent results from replaying a test case!\n" f" last: {node.transition.status.name} from {old_origin}\n" f" this: {new_transition.status.name} from {new_origin}", (old_origin, new_origin), ) else: node.transition = new_transition assert node is self._trail[-1] node.check_exhausted() assert len(node.values) > 0 or node.check_exhausted() if not self.killed: self.__update_exhausted() def __update_exhausted(self) -> None: for t in reversed(self._trail): # Any node we've traversed might have now become exhausted. # We check from the right. As soon as we hit a node that # isn't exhausted, this automatically implies that all of # its parents are not exhausted, so we stop. if not t.check_exhausted(): break ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/engine.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import importlib import math import threading import time from collections import defaultdict from collections.abc import Callable, Generator, Sequence from contextlib import AbstractContextManager, contextmanager, nullcontext from dataclasses import dataclass, field from datetime import timedelta from enum import Enum from random import Random from typing import Literal, NoReturn, cast from hypothesis import HealthCheck, Phase, Verbosity, settings as Settings from hypothesis._settings import local_settings from hypothesis.database import ExampleDatabase, choices_from_bytes, choices_to_bytes from hypothesis.errors import ( BackendCannotProceed, FlakyBackendFailure, HypothesisException, InvalidArgument, StopTest, ) from hypothesis.internal.cache import LRUReusedCache from hypothesis.internal.compat import NotRequired, TypedDict, ceil, override from hypothesis.internal.conjecture.choice import ( ChoiceConstraintsT, ChoiceKeyT, ChoiceNode, ChoiceT, ChoiceTemplate, choices_key, ) from hypothesis.internal.conjecture.data import ( ConjectureData, ConjectureResult, DataObserver, Overrun, Status, _Overrun, ) from hypothesis.internal.conjecture.datatree import ( DataTree, PreviouslyUnseenBehaviour, TreeRecordingObserver, ) from hypothesis.internal.conjecture.junkdrawer import ( ensure_free_stackframes, startswith, ) from hypothesis.internal.conjecture.pareto import NO_SCORE, ParetoFront, ParetoOptimiser from hypothesis.internal.conjecture.providers import ( AVAILABLE_PROVIDERS, HypothesisProvider, PrimitiveProvider, ) from hypothesis.internal.conjecture.shrinker import Shrinker, ShrinkPredicateT, sort_key from hypothesis.internal.escalation import InterestingOrigin from hypothesis.internal.healthcheck import fail_health_check from hypothesis.internal.observability import Observation, with_observability_callback from hypothesis.reporting import base_report, report, verbose_report # In most cases, the following constants are all Final. However, we do allow users # to monkeypatch all of these variables, which means we cannot annotate them as # Final or mypyc will inline them and render monkeypatching useless. #: The maximum number of times the shrinker will reduce the complexity of a failing #: input before giving up. This avoids falling down a trap of exponential (or worse) #: complexity, where the shrinker appears to be making progress but will take a #: substantially long time to finish completely. MAX_SHRINKS: int = 500 # If the shrinking phase takes more than five minutes, abort it early and print # a warning. Many CI systems will kill a build after around ten minutes with # no output, and appearing to hang isn't great for interactive use either - # showing partially-shrunk examples is better than quitting with no examples! # (but make it monkeypatchable, for the rare users who need to keep on shrinking) #: The maximum total time in seconds that the shrinker will try to shrink a failure #: for before giving up. This is across all shrinks for the same failure, so even #: if the shrinker successfully reduces the complexity of a single failure several #: times, it will stop when it hits |MAX_SHRINKING_SECONDS| of total time taken. MAX_SHRINKING_SECONDS: int = 300 #: The maximum amount of entropy a single test case can use before giving up #: while making random choices during input generation. #: #: The "unit" of one |BUFFER_SIZE| does not have any defined semantics, and you #: should not rely on it, except that a linear increase |BUFFER_SIZE| will linearly #: increase the amount of entropy a test case can use during generation. BUFFER_SIZE: int = 8 * 1024 CACHE_SIZE: int = 10000 MIN_TEST_CALLS: int = 10 # we use this to isolate Hypothesis from interacting with the global random, # to make it easier to reason about our global random warning logic easier (see # deprecate_random_in_strategy). _random = Random() def shortlex(s): return (len(s), s) @dataclass(slots=True, frozen=False) class HealthCheckState: valid_examples: int = field(default=0) invalid_examples: int = field(default=0) overrun_examples: int = field(default=0) draw_times: defaultdict[str, list[float]] = field( default_factory=lambda: defaultdict(list) ) @property def total_draw_time(self) -> float: return math.fsum(sum(self.draw_times.values(), start=[])) def timing_report(self) -> str: """Return a terminal report describing what was slow.""" if not self.draw_times: return "" width = max( len(k.removeprefix("generate:").removesuffix(": ")) for k in self.draw_times ) out = [f"\n {'':^{width}} count | fraction | slowest draws (seconds)"] args_in_order = sorted(self.draw_times.items(), key=lambda kv: -sum(kv[1])) for i, (argname, times) in enumerate(args_in_order): # pragma: no branch # If we have very many unique keys, which can happen due to interactive # draws with computed labels, we'll skip uninformative rows. if ( 5 <= i < (len(self.draw_times) - 2) and math.fsum(times) * 20 < self.total_draw_time ): out.append(f" (skipped {len(self.draw_times) - i} rows of fast draws)") break # Compute the row to report, omitting times <1ms to focus on slow draws reprs = [f"{t:>6.3f}," for t in sorted(times)[-5:] if t > 5e-4] desc = " ".join(([" -- "] * 5 + reprs)[-5:]).rstrip(",") arg = argname.removeprefix("generate:").removesuffix(": ") out.append( f" {arg:^{width}} | {len(times):>4} | " f"{math.fsum(times)/self.total_draw_time:>7.0%} | {desc}" ) return "\n".join(out) def _invalid_thresholds(*, r: float, c: float) -> tuple[int, int]: base = math.ceil(math.log(1 - c) / math.log(1 - r)) - 1 per_p = math.ceil(1 / r) return base, per_p # stop once we're 99% confident the true valid rate is below 1%. See # https://github.com/HypothesisWorks/hypothesis/issues/4623#issuecomment-3814681997 # for how we derived this formula. INVALID_THRESHOLD_BASE, INVALID_PER_VALID = _invalid_thresholds(r=0.01, c=0.99) class ExitReason(Enum): max_examples = "settings.max_examples={s.max_examples}" max_iterations = ( "settings.max_examples={s.max_examples}, " "but < 1% of examples satisfied assumptions" ) max_shrinks = f"shrunk example {MAX_SHRINKS} times" finished = "nothing left to do" flaky = "test was flaky" very_slow_shrinking = "shrinking was very slow" def describe(self, settings: Settings) -> str: return self.value.format(s=settings) class RunIsComplete(Exception): pass def _get_provider(backend: str) -> PrimitiveProvider | type[PrimitiveProvider]: provider_cls = AVAILABLE_PROVIDERS[backend] if isinstance(provider_cls, str): module_name, class_name = provider_cls.rsplit(".", 1) provider_cls = getattr(importlib.import_module(module_name), class_name) if provider_cls.lifetime == "test_function": return provider_cls(None) elif provider_cls.lifetime == "test_case": return provider_cls else: raise InvalidArgument( f"invalid lifetime {provider_cls.lifetime} for provider {provider_cls.__name__}. " "Expected one of 'test_function', 'test_case'." ) class CallStats(TypedDict): status: str runtime: float drawtime: float gctime: float events: list[str] PhaseStatistics = TypedDict( "PhaseStatistics", { "duration-seconds": float, "test-cases": list[CallStats], "distinct-failures": int, "shrinks-successful": int, }, ) StatisticsDict = TypedDict( "StatisticsDict", { "generate-phase": NotRequired[PhaseStatistics], "reuse-phase": NotRequired[PhaseStatistics], "shrink-phase": NotRequired[PhaseStatistics], "stopped-because": NotRequired[str], "targets": NotRequired[dict[str, float]], "nodeid": NotRequired[str], }, ) def choice_count(choices: Sequence[ChoiceT | ChoiceTemplate]) -> int | None: count = 0 for choice in choices: if isinstance(choice, ChoiceTemplate): if choice.count is None: return None count += choice.count else: count += 1 return count class DiscardObserver(DataObserver): @override def kill_branch(self) -> NoReturn: raise ContainsDiscard def realize_choices(data: ConjectureData, *, for_failure: bool) -> None: for node in data.nodes: value = data.provider.realize(node.value, for_failure=for_failure) expected_type = { "string": str, "float": float, "integer": int, "boolean": bool, "bytes": bytes, }[node.type] if type(value) is not expected_type: raise HypothesisException( f"expected {expected_type} from " f"{data.provider.realize.__qualname__}, got {type(value)}" ) constraints = cast( ChoiceConstraintsT, { k: data.provider.realize(v, for_failure=for_failure) for k, v in node.constraints.items() }, ) node.value = value node.constraints = constraints class ConjectureRunner: def __init__( self, test_function: Callable[[ConjectureData], None], *, settings: Settings | None = None, random: Random | None = None, database_key: bytes | None = None, ignore_limits: bool = False, thread_overlap: dict[int, bool] | None = None, ) -> None: self._test_function: Callable[[ConjectureData], None] = test_function self.settings: Settings = settings or Settings() self.shrinks: int = 0 self.finish_shrinking_deadline: float | None = None self.call_count: int = 0 self.misaligned_count: int = 0 self.valid_examples: int = 0 self.invalid_examples: int = 0 self.overrun_examples: int = 0 self.random: Random = random or Random(_random.getrandbits(128)) self.database_key: bytes | None = database_key self.ignore_limits: bool = ignore_limits self.thread_overlap = {} if thread_overlap is None else thread_overlap # Global dict of per-phase statistics, and a list of per-call stats # which transfer to the global dict at the end of each phase. self._current_phase: str = "(not a phase)" self.statistics: StatisticsDict = {} self.stats_per_test_case: list[CallStats] = [] self.interesting_examples: dict[InterestingOrigin, ConjectureResult] = {} # We use call_count because there may be few possible valid_examples. self.first_bug_found_at: int | None = None self.last_bug_found_at: int | None = None self.first_bug_found_time: float = math.inf self.shrunk_examples: set[InterestingOrigin] = set() self.health_check_state: HealthCheckState | None = None self.tree: DataTree = DataTree() self.provider: PrimitiveProvider | type[PrimitiveProvider] = _get_provider( self.settings.backend ) self.best_observed_targets: defaultdict[str, float] = defaultdict( lambda: NO_SCORE ) self.best_examples_of_observed_targets: dict[str, ConjectureResult] = {} # We keep the pareto front in the example database if we have one. This # is only marginally useful at present, but speeds up local development # because it means that large targets will be quickly surfaced in your # testing. self.pareto_front: ParetoFront | None = None if self.database_key is not None and self.settings.database is not None: self.pareto_front = ParetoFront(self.random) self.pareto_front.on_evict(self.on_pareto_evict) # We want to be able to get the ConjectureData object that results # from running a choice sequence without recalculating, especially during # shrinking where we need to know about the structure of the # executed test case. self.__data_cache = LRUReusedCache[ tuple[ChoiceKeyT, ...], ConjectureResult | _Overrun ](CACHE_SIZE) self.reused_previously_shrunk_test_case: bool = False self.__pending_call_explanation: str | None = None self._backend_found_failure: bool = False self._backend_exceeded_deadline: bool = False self._backend_discard_count: int = 0 # note unsound verification by alt backends self._verified_by_backend: str | None = None self._switch_to_hypothesis_provider: bool = False @contextmanager def _with_switch_to_hypothesis_provider( self, value: bool ) -> Generator[None, None, None]: previous = self._switch_to_hypothesis_provider try: self._switch_to_hypothesis_provider = value yield finally: self._switch_to_hypothesis_provider = previous @property def using_hypothesis_backend(self) -> bool: return ( self.settings.backend == "hypothesis" or self._switch_to_hypothesis_provider ) def explain_next_call_as(self, explanation: str) -> None: self.__pending_call_explanation = explanation def clear_call_explanation(self) -> None: self.__pending_call_explanation = None @contextmanager def _log_phase_statistics( self, phase: Literal["reuse", "generate", "shrink"] ) -> Generator[None, None, None]: self.stats_per_test_case.clear() start_time = time.perf_counter() try: self._current_phase = phase yield finally: self.statistics[phase + "-phase"] = { # type: ignore "duration-seconds": time.perf_counter() - start_time, "test-cases": list(self.stats_per_test_case), "distinct-failures": len(self.interesting_examples), "shrinks-successful": self.shrinks, } @property def should_optimise(self) -> bool: return Phase.target in self.settings.phases def __tree_is_exhausted(self) -> bool: return self.tree.is_exhausted and self.using_hypothesis_backend def __stoppable_test_function(self, data: ConjectureData) -> None: """Run ``self._test_function``, but convert a ``StopTest`` exception into a normal return and avoid raising anything flaky for RecursionErrors. """ # We ensure that the test has this much stack space remaining, no # matter the size of the stack when called, to de-flake RecursionErrors # (#2494, #3671). Note, this covers the data generation part of the test; # the actual test execution is additionally protected at the call site # in hypothesis.core.execute_once. with ensure_free_stackframes(): try: self._test_function(data) except StopTest as e: if e.testcounter == data.testcounter: # This StopTest has successfully stopped its test, and can now # be discarded. pass else: # This StopTest was raised by a different ConjectureData. We # need to re-raise it so that it will eventually reach the # correct engine. raise def _cache_key(self, choices: Sequence[ChoiceT]) -> tuple[ChoiceKeyT, ...]: return choices_key(choices) def _cache(self, data: ConjectureData) -> None: result = data.as_result() key = self._cache_key(data.choices) self.__data_cache[key] = result def cached_test_function( self, choices: Sequence[ChoiceT | ChoiceTemplate], *, error_on_discard: bool = False, extend: int | Literal["full"] = 0, ) -> ConjectureResult | _Overrun: """ If ``error_on_discard`` is set to True this will raise ``ContainsDiscard`` in preference to running the actual test function. This is to allow us to skip test cases we expect to be redundant in some cases. Note that it may be the case that we don't raise ``ContainsDiscard`` even if the result has discards if we cannot determine from previous runs whether it will have a discard. """ # node templates represent a not-yet-filled hole and therefore cannot # be cached or retrieved from the cache. if not any(isinstance(choice, ChoiceTemplate) for choice in choices): # this type cast is validated by the isinstance check above (ie, there # are no ChoiceTemplate elements). choices = cast(Sequence[ChoiceT], choices) key = self._cache_key(choices) try: cached = self.__data_cache[key] # if we have a cached overrun for this key, but we're allowing extensions # of the nodes, it could in fact run to a valid data if we try. if extend == 0 or cached.status is not Status.OVERRUN: return cached except KeyError: pass if extend == "full": max_length = None elif (count := choice_count(choices)) is None: max_length = None else: max_length = count + extend # explicitly use a no-op DataObserver here instead of a TreeRecordingObserver. # The reason is we don't expect simulate_test_function to explore new choices # and write back to the tree, so we don't want the overhead of the # TreeRecordingObserver tracking those calls. trial_observer: DataObserver | None = DataObserver() if error_on_discard: trial_observer = DiscardObserver() try: trial_data = self.new_conjecture_data( choices, observer=trial_observer, max_choices=max_length ) self.tree.simulate_test_function(trial_data) except PreviouslyUnseenBehaviour: pass else: trial_data.freeze() key = self._cache_key(trial_data.choices) if trial_data.status > Status.OVERRUN: try: return self.__data_cache[key] except KeyError: pass else: # if we simulated to an overrun, then we our result is certainly # an overrun; no need to consult the cache. (and we store this result # for simulation-less lookup later). self.__data_cache[key] = Overrun return Overrun try: return self.__data_cache[key] except KeyError: pass data = self.new_conjecture_data(choices, max_choices=max_length) # note that calling test_function caches `data` for us. self.test_function(data) return data.as_result() def test_function(self, data: ConjectureData) -> None: if self.__pending_call_explanation is not None: self.debug(self.__pending_call_explanation) self.__pending_call_explanation = None self.call_count += 1 interrupted = False def _backend_cannot_proceed( exc: BackendCannotProceed, data: ConjectureData ) -> None: if exc.scope in ("verified", "exhausted"): self._switch_to_hypothesis_provider = True if exc.scope == "verified": self._verified_by_backend = self.settings.backend elif exc.scope == "discard_test_case": self._backend_discard_count += 1 if ( self._backend_discard_count > 10 and (self._backend_discard_count / self.call_count) > 0.2 ): verbose_report( f"Switching away from backend {self.settings.backend!r} " "to the Hypothesis backend, " f"because {self._backend_discard_count} of {self.call_count} " "attempted test cases " f"({self._backend_discard_count / self.call_count * 100:0.1f}%) " f"were discarded by backend {self.settings.backend!r}" ) self._switch_to_hypothesis_provider = True # treat all BackendCannotProceed exceptions as invalid. This isn't # great; "verified" should really be counted as self.valid_examples += 1. # But we check self.valid_examples == 0 to determine whether to raise # Unsatisfiable, and that would throw this check off. self.invalid_examples += 1 data.cannot_proceed_scope = exc.scope # this fiddly bit of control flow is to work around `return` being # disallowed in `finally` blocks as of python 3.14. Otherwise, we would # just return in the _backend_cannot_proceed branch. finally_early_return = False try: self.__stoppable_test_function(data) except KeyboardInterrupt: interrupted = True raise except BackendCannotProceed as exc: _backend_cannot_proceed(exc, data) # skip the post-test-case tracking; we're pretending this never happened interrupted = True data.freeze() return except BaseException: data.freeze() if self.settings.backend != "hypothesis": try: realize_choices(data, for_failure=True) except BackendCannotProceed as exc: _backend_cannot_proceed(exc, data) # skip the post-test-case tracking; we're pretending this # never happened interrupted = True return self.save_choices(data.choices) raise finally: # No branch, because if we're interrupted we always raise # the KeyboardInterrupt, never continue to the code below. if not interrupted: # pragma: no branch assert data.cannot_proceed_scope is None data.freeze() if self.settings.backend != "hypothesis": try: realize_choices( data, for_failure=data.status is Status.INTERESTING ) except BackendCannotProceed as exc: _backend_cannot_proceed(exc, data) finally_early_return = True if not finally_early_return: call_stats: CallStats = { "status": data.status.name.lower(), "runtime": data.finish_time - data.start_time, "drawtime": math.fsum(data.draw_times.values()), "gctime": data.gc_finish_time - data.gc_start_time, "events": sorted( k if v == "" else f"{k}: {v}" for k, v in data.events.items() ), } self.stats_per_test_case.append(call_stats) self._cache(data) if ( data.misaligned_at is not None ): # pragma: no branch # coverage bug? self.misaligned_count += 1 if finally_early_return: return self.debug_data(data) if ( data.target_observations and self.pareto_front is not None and self.pareto_front.add(data.as_result()) ): self.save_choices(data.choices, sub_key=b"pareto") if data.status >= Status.VALID: for k, v in data.target_observations.items(): self.best_observed_targets[k] = max(self.best_observed_targets[k], v) if k not in self.best_examples_of_observed_targets: data_as_result = data.as_result() assert not isinstance(data_as_result, _Overrun) self.best_examples_of_observed_targets[k] = data_as_result continue existing_example = self.best_examples_of_observed_targets[k] existing_score = existing_example.target_observations[k] if v < existing_score: continue if v > existing_score or sort_key(data.nodes) < sort_key( existing_example.nodes ): data_as_result = data.as_result() assert not isinstance(data_as_result, _Overrun) self.best_examples_of_observed_targets[k] = data_as_result if data.status is Status.VALID: self.valid_examples += 1 if data.status is Status.INVALID: self.invalid_examples += 1 if data.status is Status.OVERRUN: self.overrun_examples += 1 if data.status == Status.INTERESTING: if not self.using_hypothesis_backend: # replay this failure on the hypothesis backend to ensure it still # finds a failure. otherwise, it is flaky. initial_exception = data.expected_exception data = ConjectureData.for_choices(data.choices) # we've already going to use the hypothesis provider for this # data, so the verb "switch" is a bit misleading here. We're really # setting this to inform our on_observation logic that the observation # generated here was from a hypothesis backend, and shouldn't be # sent to the on_observation of any alternative backend. with self._with_switch_to_hypothesis_provider(True): self.__stoppable_test_function(data) data.freeze() # TODO: Should same-origin also be checked? (discussion in # https://github.com/HypothesisWorks/hypothesis/pull/4470#discussion_r2217055487) if data.status != Status.INTERESTING: desc_new_status = { data.status.VALID: "passed", data.status.INVALID: "failed filters", data.status.OVERRUN: "overran", }[data.status] raise FlakyBackendFailure( f"Inconsistent results from replaying a failing test case! " f"Raised {type(initial_exception).__name__} on " f"backend={self.settings.backend!r}, but " f"{desc_new_status} under backend='hypothesis'.", [initial_exception], ) self._cache(data) assert data.interesting_origin is not None key = data.interesting_origin changed = False try: existing = self.interesting_examples[key] except KeyError: changed = True self.last_bug_found_at = self.call_count if self.first_bug_found_at is None: self.first_bug_found_at = self.call_count self.first_bug_found_time = time.monotonic() else: if sort_key(data.nodes) < sort_key(existing.nodes): self.shrinks += 1 self.downgrade_choices(existing.choices) self.__data_cache.unpin(self._cache_key(existing.choices)) changed = True if changed: self.save_choices(data.choices) self.interesting_examples[key] = data.as_result() # type: ignore if not self.using_hypothesis_backend: self._backend_found_failure = True self.__data_cache.pin(self._cache_key(data.choices), data.as_result()) self.shrunk_examples.discard(key) if self.shrinks >= MAX_SHRINKS: self.exit_with(ExitReason.max_shrinks) if ( not self.ignore_limits and self.finish_shrinking_deadline is not None and self.finish_shrinking_deadline < time.perf_counter() ): # See https://github.com/HypothesisWorks/hypothesis/issues/2340 report( "WARNING: Hypothesis has spent more than five minutes working to shrink" " a failing example, and stopped because it is making very slow" " progress. When you re-run your tests, shrinking will resume and may" " take this long before aborting again.\nPLEASE REPORT THIS if you can" " provide a reproducing example, so that we can improve shrinking" " performance for everyone." ) self.exit_with(ExitReason.very_slow_shrinking) if not self.interesting_examples: # Note that this logic is reproduced to end the generation phase when # we have interesting examples. Update that too if you change this! # (The doubled implementation is because here we exit the engine entirely, # while in the other case below we just want to move on to shrinking.) if self.valid_examples >= self.settings.max_examples: self.exit_with(ExitReason.max_examples) if (self.invalid_examples + self.overrun_examples) > ( INVALID_THRESHOLD_BASE + INVALID_PER_VALID * self.valid_examples ): self.exit_with(ExitReason.max_iterations) if self.__tree_is_exhausted(): self.exit_with(ExitReason.finished) self.record_for_health_check(data) def on_pareto_evict(self, data: ConjectureResult) -> None: self.settings.database.delete(self.pareto_key, choices_to_bytes(data.choices)) def generate_novel_prefix(self) -> tuple[ChoiceT, ...]: """Uses the tree to proactively generate a starting choice sequence that we haven't explored yet for this test. When this method is called, we assume that there must be at least one novel prefix left to find. If there were not, then the test run should have already stopped due to tree exhaustion. """ return self.tree.generate_novel_prefix(self.random) def record_for_health_check(self, data: ConjectureData) -> None: # Once we've actually found a bug, there's no point in trying to run # health checks - they'll just mask the actually important information. if data.status == Status.INTERESTING: self.health_check_state = None state = self.health_check_state if state is None: return for k, v in data.draw_times.items(): state.draw_times[k].append(v) if data.status == Status.VALID: state.valid_examples += 1 elif data.status == Status.INVALID: state.invalid_examples += 1 else: assert data.status == Status.OVERRUN state.overrun_examples += 1 max_valid_draws = 10 max_invalid_draws = 50 max_overrun_draws = 20 assert state.valid_examples <= max_valid_draws if state.valid_examples == max_valid_draws: self.health_check_state = None return if state.overrun_examples == max_overrun_draws: fail_health_check( self.settings, "Generated inputs routinely consumed more than the maximum " f"allowed entropy: {state.valid_examples} inputs were generated " f"successfully, while {state.overrun_examples} inputs exceeded the " f"maximum allowed entropy during generation." "\n\n" f"Testing with inputs this large tends to be slow, and to produce " "failures that are both difficult to shrink and difficult to understand. " "Try decreasing the amount of data generated, for example by " "decreasing the minimum size of collection strategies like " "st.lists()." "\n\n" "If you expect the average size of your input to be this large, " "you can disable this health check with " "@settings(suppress_health_check=[HealthCheck.data_too_large]). " "See " "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " "for details.", HealthCheck.data_too_large, ) if state.invalid_examples == max_invalid_draws: fail_health_check( self.settings, "It looks like this test is filtering out a lot of inputs. " f"{state.valid_examples} inputs were generated successfully, " f"while {state.invalid_examples} inputs were filtered out. " "\n\n" "An input might be filtered out by calls to assume(), " "strategy.filter(...), or occasionally by Hypothesis internals." "\n\n" "Applying this much filtering makes input generation slow, since " "Hypothesis must discard inputs which are filtered out and try " "generating it again. It is also possible that applying this much " "filtering will distort the domain and/or distribution of the test, " "leaving your testing less rigorous than expected." "\n\n" "If you expect this many inputs to be filtered out during generation, " "you can disable this health check with " "@settings(suppress_health_check=[HealthCheck.filter_too_much]). See " "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " "for details.", HealthCheck.filter_too_much, ) # Allow at least the greater of one second or 5x the deadline. If deadline # is None, allow 30s - the user can disable the healthcheck too if desired. draw_time = state.total_draw_time draw_time_limit = 5 * (self.settings.deadline or timedelta(seconds=6)) if ( draw_time > max(1.0, draw_time_limit.total_seconds()) # we disable HealthCheck.too_slow under concurrent threads, since # cpython may switch away from a thread for arbitrarily long. and not self.thread_overlap.get(threading.get_ident(), False) ): extra_str = [] if state.invalid_examples: extra_str.append(f"{state.invalid_examples} invalid inputs") if state.overrun_examples: extra_str.append( f"{state.overrun_examples} inputs which exceeded the " "maximum allowed entropy" ) extra_str = ", and ".join(extra_str) extra_str = f" ({extra_str})" if extra_str else "" fail_health_check( self.settings, "Input generation is slow: Hypothesis only generated " f"{state.valid_examples} valid inputs after {draw_time:.2f} " f"seconds{extra_str}." "\n" + state.timing_report() + "\n\n" "This could be for a few reasons:" "\n" "1. This strategy could be generating too much data per input. " "Try decreasing the amount of data generated, for example by " "decreasing the minimum size of collection strategies like " "st.lists()." "\n" "2. Some other expensive computation could be running during input " "generation. For example, " "if @st.composite or st.data() is interspersed with an expensive " "computation, HealthCheck.too_slow is likely to trigger. If this " "computation is unrelated to input generation, move it elsewhere. " "Otherwise, try making it more efficient, or disable this health " "check if that is not possible." "\n\n" "If you expect input generation to take this long, you can disable " "this health check with " "@settings(suppress_health_check=[HealthCheck.too_slow]). See " "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " "for details.", HealthCheck.too_slow, ) def save_choices( self, choices: Sequence[ChoiceT], sub_key: bytes | None = None ) -> None: if self.settings.database is not None: key = self.sub_key(sub_key) if key is None: return self.settings.database.save(key, choices_to_bytes(choices)) def downgrade_choices(self, choices: Sequence[ChoiceT]) -> None: buffer = choices_to_bytes(choices) if self.settings.database is not None and self.database_key is not None: self.settings.database.move(self.database_key, self.secondary_key, buffer) def sub_key(self, sub_key: bytes | None) -> bytes | None: if self.database_key is None: return None if sub_key is None: return self.database_key return b".".join((self.database_key, sub_key)) @property def secondary_key(self) -> bytes | None: return self.sub_key(b"secondary") @property def pareto_key(self) -> bytes | None: return self.sub_key(b"pareto") def debug(self, message: str) -> None: if self.settings.verbosity >= Verbosity.debug: base_report(message) @property def report_debug_info(self) -> bool: return self.settings.verbosity >= Verbosity.debug def debug_data(self, data: ConjectureData | ConjectureResult) -> None: if not self.report_debug_info: return status = repr(data.status) if data.status == Status.INTERESTING: status = f"{status} ({data.interesting_origin!r})" elif data.status == Status.INVALID and isinstance(data, ConjectureData): assert isinstance(data, ConjectureData) # mypy is silly status = f"{status} ({data.events.get('invalid because', '?')})" newline_tab = "\n\t" self.debug( f"{len(data.choices)} choices -> {status}\n\t{data.choices}" f"{newline_tab + data.output if data.output else ''}" ) def observe_for_provider(self) -> AbstractContextManager: def on_observation(observation: Observation) -> None: assert observation.type == "test_case" # because lifetime == "test_function" assert isinstance(self.provider, PrimitiveProvider) # only fire if we actually used that provider to generate this observation if not self._switch_to_hypothesis_provider: self.provider.on_observation(observation) if ( self.settings.backend != "hypothesis" # only for lifetime = "test_function" providers (guaranteed # by this isinstance check) and isinstance(self.provider, PrimitiveProvider) # and the provider opted-in to observations and self.provider.add_observability_callback ): return with_observability_callback(on_observation) return nullcontext() def run(self) -> None: with local_settings(self.settings), self.observe_for_provider(): try: self._run() except RunIsComplete: pass for v in self.interesting_examples.values(): self.debug_data(v) self.debug( f"Run complete after {self.call_count} examples " f"({self.valid_examples} valid) and {self.shrinks} shrinks" ) @property def database(self) -> ExampleDatabase | None: if self.database_key is None: return None return self.settings.database def has_existing_examples(self) -> bool: return self.database is not None and Phase.reuse in self.settings.phases def reuse_existing_examples(self) -> None: """If appropriate (we have a database and have been told to use it), try to reload existing examples from the database. If there are a lot we don't try all of them. We always try the smallest example in the database (which is guaranteed to be the last failure) and the largest (which is usually the seed example which the last failure came from but we don't enforce that). We then take a random sampling of the remainder and try those. Any examples that are no longer interesting are cleared out. """ if self.has_existing_examples(): self.debug("Reusing examples from database") # We have to do some careful juggling here. We have two database # corpora: The primary and secondary. The primary corpus is a # small set of minimized examples each of which has at one point # demonstrated a distinct bug. We want to retry all of these. # We also have a secondary corpus of examples that have at some # point demonstrated interestingness (currently only ones that # were previously non-minimal examples of a bug, but this will # likely expand in future). These are a good source of potentially # interesting examples, but there are a lot of them, so we down # sample the secondary corpus to a more manageable size. corpus = sorted( self.settings.database.fetch(self.database_key), key=shortlex ) factor = 0.1 if (Phase.generate in self.settings.phases) else 1 desired_size = max(2, ceil(factor * self.settings.max_examples)) primary_corpus_size = len(corpus) if len(corpus) < desired_size: extra_corpus = list(self.settings.database.fetch(self.secondary_key)) shortfall = desired_size - len(corpus) if len(extra_corpus) <= shortfall: extra = extra_corpus else: extra = self.random.sample(extra_corpus, shortfall) extra.sort(key=shortlex) corpus.extend(extra) # We want a fast path where every primary entry in the database was # interesting. found_interesting_in_primary = False all_interesting_in_primary_were_exact = True for i, existing in enumerate(corpus): if i >= primary_corpus_size and found_interesting_in_primary: break choices = choices_from_bytes(existing) if choices is None: # clear out any keys which fail deserialization self.settings.database.delete(self.database_key, existing) continue data = self.cached_test_function(choices, extend="full") if data.status != Status.INTERESTING: self.settings.database.delete(self.database_key, existing) self.settings.database.delete(self.secondary_key, existing) else: if i < primary_corpus_size: found_interesting_in_primary = True assert not isinstance(data, _Overrun) if choices_key(choices) != choices_key(data.choices): all_interesting_in_primary_were_exact = False if not self.settings.report_multiple_bugs: break if found_interesting_in_primary: if all_interesting_in_primary_were_exact: self.reused_previously_shrunk_test_case = True # Because self.database is not None (because self.has_existing_examples()) # and self.database_key is not None (because we fetched using it above), # we can guarantee self.pareto_front is not None assert self.pareto_front is not None # If we've not found any interesting examples so far we try some of # the pareto front from the last run. if len(corpus) < desired_size and not self.interesting_examples: desired_extra = desired_size - len(corpus) pareto_corpus = list(self.settings.database.fetch(self.pareto_key)) if len(pareto_corpus) > desired_extra: pareto_corpus = self.random.sample(pareto_corpus, desired_extra) pareto_corpus.sort(key=shortlex) for existing in pareto_corpus: choices = choices_from_bytes(existing) if choices is None: self.settings.database.delete(self.pareto_key, existing) continue data = self.cached_test_function(choices, extend="full") if data not in self.pareto_front: self.settings.database.delete(self.pareto_key, existing) if data.status == Status.INTERESTING: break def exit_with(self, reason: ExitReason) -> None: if self.ignore_limits: return self.statistics["stopped-because"] = reason.describe(self.settings) if self.best_observed_targets: self.statistics["targets"] = dict(self.best_observed_targets) self.debug(f"exit_with({reason.name})") self.exit_reason = reason raise RunIsComplete def should_generate_more(self) -> bool: # End the generation phase where we would have ended it if no bugs had # been found. This reproduces the exit logic in `self.test_function`, # but with the important distinction that this clause will move on to # the shrinking phase having found one or more bugs, while the other # will exit having found zero bugs. invalid_threshold = ( INVALID_THRESHOLD_BASE + INVALID_PER_VALID * self.valid_examples ) if ( self.valid_examples >= self.settings.max_examples or (self.invalid_examples + self.overrun_examples) > invalid_threshold ): # pragma: no cover return False # If we haven't found a bug, keep looking - if we hit any limits on # the number of tests to run that will raise an exception and stop # the run. if not self.interesting_examples: return True # Users who disable shrinking probably want to exit as fast as possible. # If we've found a bug and won't report more than one, stop looking. # If we first saw a bug more than 10 seconds ago, stop looking. elif ( Phase.shrink not in self.settings.phases or not self.settings.report_multiple_bugs or time.monotonic() - self.first_bug_found_time > 10 ): return False assert isinstance(self.first_bug_found_at, int) assert isinstance(self.last_bug_found_at, int) assert self.first_bug_found_at <= self.last_bug_found_at <= self.call_count # Otherwise, keep searching for between ten and 'a heuristic' calls. # We cap 'calls after first bug' so errors are reported reasonably # soon even for tests that are allowed to run for a very long time, # or sooner if the latest half of our test effort has been fruitless. return self.call_count < MIN_TEST_CALLS or self.call_count < min( self.first_bug_found_at + 1000, self.last_bug_found_at * 2 ) def generate_new_examples(self) -> None: if Phase.generate not in self.settings.phases: return if self.interesting_examples: # The example database has failing examples from a previous run, # so we'd rather report that they're still failing ASAP than take # the time to look for additional failures. return self.debug("Generating new examples") assert self.should_generate_more() self._switch_to_hypothesis_provider = True zero_data = self.cached_test_function((ChoiceTemplate("simplest", count=None),)) if zero_data.status > Status.OVERRUN: assert isinstance(zero_data, ConjectureResult) # if the crosshair backend cannot proceed, it does not (and cannot) # realize the symbolic values, with the intent that Hypothesis will # throw away this test case. We usually do, but if it's the zero data # then we try to pin it here, which requires realizing the symbolics. # # We don't (yet) rely on the zero data being pinned, and so # it's simply a very slight performance loss to simply not pin it # if doing so would error. if zero_data.cannot_proceed_scope is None: # pragma: no branch self.__data_cache.pin( self._cache_key(zero_data.choices), zero_data.as_result() ) # Pin forever if zero_data.status == Status.OVERRUN or ( zero_data.status == Status.VALID and isinstance(zero_data, ConjectureResult) and zero_data.length * 2 > BUFFER_SIZE ): fail_health_check( self.settings, "The smallest natural input for this test is very " "large. This makes it difficult for Hypothesis to generate " "good inputs, especially when trying to shrink failing inputs." "\n\n" "Consider reducing the amount of data generated by the strategy. " "Also consider introducing small alternative values for some " "strategies. For example, could you " "mark some arguments as optional by replacing `some_complex_strategy`" "with `st.none() | some_complex_strategy`?" "\n\n" "If you are confident that the size of the smallest natural input " "to your test cannot be reduced, you can suppress this health check " "with @settings(suppress_health_check=[HealthCheck.large_base_example]). " "See " "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck " "for details.", HealthCheck.large_base_example, ) self.health_check_state = HealthCheckState() # We attempt to use the size of the minimal generated test case starting # from a given novel prefix as a guideline to generate smaller test # cases for an initial period, by restriscting ourselves to test cases # that are not much larger than it. # # Calculating the actual minimal generated test case is hard, so we # take a best guess that zero extending a prefix produces the minimal # test case starting with that prefix (this is true for our built in # strategies). This is only a reasonable thing to do if the resulting # test case is valid. If we regularly run into situations where it is # not valid then this strategy is a waste of time, so we want to # abandon it early. In order to do this we track how many times in a # row it has failed to work, and abort small test case generation when # it has failed too many times in a row. consecutive_zero_extend_is_invalid = 0 # We control growth during initial example generation, for two # reasons: # # * It gives us an opportunity to find small examples early, which # gives us a fast path for easy to find bugs. # * It avoids low probability events where we might end up # generating very large examples during health checks, which # on slower machines can trigger HealthCheck.too_slow. # # The heuristic we use is that we attempt to estimate the smallest # extension of this prefix, and limit the size to no more than # an order of magnitude larger than that. If we fail to estimate # the size accurately, we skip over this prefix and try again. # # We need to tune the example size based on the initial prefix, # because any fixed size might be too small, and any size based # on the strategy in general can fall afoul of strategies that # have very different sizes for different prefixes. # # We previously set a minimum value of 10 on small_example_cap, with the # reasoning of avoiding flaky health checks. However, some users set a # low max_examples for performance. A hard lower bound in this case biases # the distribution towards small (and less powerful) examples. Flaky # and loud health checks are better than silent performance degradation. small_example_cap = min(self.settings.max_examples // 10, 50) optimise_at = max(self.settings.max_examples // 2, small_example_cap + 1, 10) ran_optimisations = False self._switch_to_hypothesis_provider = False while self.should_generate_more(): # we don't yet integrate DataTree with backends. Instead of generating # a novel prefix, ask the backend for an input. if not self.using_hypothesis_backend: data = self.new_conjecture_data([]) self.test_function(data) continue self._current_phase = "generate" prefix = self.generate_novel_prefix() if ( self.valid_examples <= small_example_cap and self.call_count <= 5 * small_example_cap and not self.interesting_examples and consecutive_zero_extend_is_invalid < 5 ): minimal_example = self.cached_test_function( prefix + (ChoiceTemplate("simplest", count=None),) ) if minimal_example.status < Status.VALID: consecutive_zero_extend_is_invalid += 1 continue # Because the Status code is greater than Status.VALID, it cannot be # Status.OVERRUN, which guarantees that the minimal_example is a # ConjectureResult object. assert isinstance(minimal_example, ConjectureResult) consecutive_zero_extend_is_invalid = 0 minimal_extension = len(minimal_example.choices) - len(prefix) max_length = len(prefix) + minimal_extension * 5 # We could end up in a situation where even though the prefix was # novel when we generated it, because we've now tried zero extending # it not all possible continuations of it will be novel. In order to # avoid making redundant test calls, we rerun it in simulation mode # first. If this has a predictable result, then we don't bother # running the test function for real here. If however we encounter # some novel behaviour, we try again with the real test function, # starting from the new novel prefix that has discovered. trial_data = self.new_conjecture_data(prefix, max_choices=max_length) try: self.tree.simulate_test_function(trial_data) continue except PreviouslyUnseenBehaviour: pass # If the simulation entered part of the tree that has been killed, # we don't want to run this. assert isinstance(trial_data.observer, TreeRecordingObserver) if trial_data.observer.killed: continue # We might have hit the cap on number of examples we should # run when calculating the minimal example. if not self.should_generate_more(): break prefix = trial_data.choices else: max_length = None data = self.new_conjecture_data(prefix, max_choices=max_length) self.test_function(data) if ( data.status is Status.OVERRUN and max_length is not None and "invalid because" not in data.events ): data.events["invalid because"] = ( "reduced max size for early examples (avoids flaky health checks)" ) self.generate_mutations_from(data) # Although the optimisations are logically a distinct phase, we # actually normally run them as part of example generation. The # reason for this is that we cannot guarantee that optimisation # actually exhausts our budget: It might finish running and we # discover that actually we still could run a bunch more test cases # if we want. if ( self.valid_examples >= max(small_example_cap, optimise_at) and not ran_optimisations ): ran_optimisations = True self._current_phase = "target" self.optimise_targets() def generate_mutations_from(self, data: ConjectureData | ConjectureResult) -> None: # A thing that is often useful but rarely happens by accident is # to generate the same value at multiple different points in the # test case. # # Rather than make this the responsibility of individual strategies # we implement a small mutator that just takes parts of the test # case with the same label and tries replacing one of them with a # copy of the other and tries running it. If we've made a good # guess about what to put where, this will run a similar generated # test case with more duplication. if ( # An OVERRUN doesn't have enough information about the test # case to mutate, so we just skip those. data.status >= Status.INVALID # This has a tendency to trigger some weird edge cases during # generation so we don't let it run until we're done with the # health checks. and self.health_check_state is None ): initial_calls = self.call_count failed_mutations = 0 while ( self.should_generate_more() # We implement fairly conservative checks for how long we # we should run mutation for, as it's generally not obvious # how helpful it is for any given test case. and self.call_count <= initial_calls + 5 and failed_mutations <= 5 ): groups = data.spans.mutator_groups if not groups: break group = self.random.choice(groups) (start1, end1), (start2, end2) = self.random.sample(sorted(group), 2) if start1 > start2: (start1, end1), (start2, end2) = (start2, end2), (start1, end1) if ( start1 <= start2 <= end2 <= end1 ): # pragma: no cover # flaky on conjecture-cover tests # One span entirely contains the other. The strategy is very # likely some kind of tree. e.g. we might have # # ┌─────┐ # ┌─────┤ a ├──────┐ # │ └─────┘ │ # ┌──┴──┐ ┌──┴──┐ # ┌──┤ b ├──┐ ┌──┤ c ├──┐ # │ └──┬──┘ │ │ └──┬──┘ │ # ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ # │ d │ │ e │ │ f │ │ g │ │ h │ │ i │ # └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ # # where each node is drawn from the same strategy and so # has the same span label. We might have selected the spans # corresponding to the a and c nodes, which is the entire # tree and the subtree of (and including) c respectively. # # There are two possible mutations we could apply in this case: # 1. replace a with c (replace child with parent) # 2. replace c with a (replace parent with child) # # (1) results in multiple partial copies of the # parent: # ┌─────┐ # ┌─────┤ a ├────────────┐ # │ └─────┘ │ # ┌──┴──┐ ┌─┴───┐ # ┌──┤ b ├──┐ ┌─────┤ a ├──────┐ # │ └──┬──┘ │ │ └─────┘ │ # ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌──┴──┐ ┌──┴──┐ # │ d │ │ e │ │ f │ ┌──┤ b ├──┐ ┌──┤ c ├──┐ # └───┘ └───┘ └───┘ │ └──┬──┘ │ │ └──┬──┘ │ # ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ # │ d │ │ e │ │ f │ │ g │ │ h │ │ i │ # └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ # # While (2) results in truncating part of the parent: # # ┌─────┐ # ┌──┤ c ├──┐ # │ └──┬──┘ │ # ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ # │ g │ │ h │ │ i │ # └───┘ └───┘ └───┘ # # (1) is the same as Example IV.4. in Nautilus (NDSS '19) # (https://wcventure.github.io/FuzzingPaper/Paper/NDSS19_Nautilus.pdf), # except we do not repeat the replacement additional times # (the paper repeats it once for a total of two copies). # # We currently only apply mutation (1), and ignore mutation # (2). The reason is that the attempt generated from (2) is # always something that Hypothesis could easily have generated # itself, by simply not making various choices. Whereas # duplicating the exact value + structure of particular choices # in (1) would have been hard for Hypothesis to generate by # chance. # # TODO: an extension of this mutation might repeat (1) on # a geometric distribution between 0 and ~10 times. We would # need to find the corresponding span to recurse on in the new # choices, probably just by using the choices index. # case (1): duplicate the choices in start1:start2. attempt = data.choices[:start2] + data.choices[start1:] else: start, end = self.random.choice([(start1, end1), (start2, end2)]) replacement = data.choices[start:end] # We attempt to replace both the examples with # whichever choice we made. Note that this might end # up messing up and getting the example boundaries # wrong - labels matching are only a best guess as to # whether the two are equivalent - but it doesn't # really matter. It may not achieve the desired result, # but it's still a perfectly acceptable choice sequence # to try. attempt = ( data.choices[:start1] + replacement + data.choices[end1:start2] + replacement + data.choices[end2:] ) try: new_data = self.cached_test_function( attempt, # We set error_on_discard so that we don't end up # entering parts of the tree we consider redundant # and not worth exploring. error_on_discard=True, ) except ContainsDiscard: failed_mutations += 1 continue if new_data is Overrun: failed_mutations += 1 # pragma: no cover # annoying case else: assert isinstance(new_data, ConjectureResult) if ( new_data.status >= data.status and choices_key(data.choices) != choices_key(new_data.choices) and all( k in new_data.target_observations and new_data.target_observations[k] >= v for k, v in data.target_observations.items() ) ): data = new_data failed_mutations = 0 else: failed_mutations += 1 def optimise_targets(self) -> None: """If any target observations have been made, attempt to optimise them all.""" if not self.should_optimise: return from hypothesis.internal.conjecture.optimiser import Optimiser # We want to avoid running the optimiser for too long in case we hit # an unbounded target score. We start this off fairly conservatively # in case interesting examples are easy to find and then ramp it up # on an exponential schedule so we don't hamper the optimiser too much # if it needs a long time to find good enough improvements. max_improvements = 10 while True: prev_calls = self.call_count any_improvements = False for target, data in list(self.best_examples_of_observed_targets.items()): optimiser = Optimiser( self, data, target, max_improvements=max_improvements ) optimiser.run() if optimiser.improvements > 0: any_improvements = True if self.interesting_examples: break max_improvements *= 2 if any_improvements: continue if self.best_observed_targets: self.pareto_optimise() if prev_calls == self.call_count: break def pareto_optimise(self) -> None: if self.pareto_front is not None: ParetoOptimiser(self).run() def _run(self) -> None: # have to use the primitive provider to interpret database bits... self._switch_to_hypothesis_provider = True with self._log_phase_statistics("reuse"): self.reuse_existing_examples() # Fast path for development: If the database gave us interesting # examples from the previously stored primary key, don't try # shrinking it again as it's unlikely to work. if self.reused_previously_shrunk_test_case: self.exit_with(ExitReason.finished) # ...but we should use the supplied provider when generating... self._switch_to_hypothesis_provider = False with self._log_phase_statistics("generate"): self.generate_new_examples() # We normally run the targeting phase mixed in with the generate phase, # but if we've been asked to run it but not generation then we have to # run it explicitly on its own here. if Phase.generate not in self.settings.phases: self._current_phase = "target" self.optimise_targets() # ...and back to the primitive provider when shrinking. self._switch_to_hypothesis_provider = True with self._log_phase_statistics("shrink"): self.shrink_interesting_examples() self.exit_with(ExitReason.finished) def new_conjecture_data( self, prefix: Sequence[ChoiceT | ChoiceTemplate], *, observer: DataObserver | None = None, max_choices: int | None = None, ) -> ConjectureData: provider = ( HypothesisProvider if self._switch_to_hypothesis_provider else self.provider ) observer = observer or self.tree.new_observer() if not self.using_hypothesis_backend: observer = DataObserver() return ConjectureData( prefix=prefix, observer=observer, provider=provider, max_choices=max_choices, random=self.random, ) def shrink_interesting_examples(self) -> None: """If we've found interesting examples, try to replace each of them with a minimal interesting example with the same interesting_origin. We may find one or more examples with a new interesting_origin during the shrink process. If so we shrink these too. """ if Phase.shrink not in self.settings.phases or not self.interesting_examples: return self.debug("Shrinking interesting examples") self.finish_shrinking_deadline = time.perf_counter() + MAX_SHRINKING_SECONDS for prev_data in sorted( self.interesting_examples.values(), key=lambda d: sort_key(d.nodes) ): assert prev_data.status == Status.INTERESTING data = self.new_conjecture_data(prev_data.choices) self.test_function(data) if data.status != Status.INTERESTING: self.exit_with(ExitReason.flaky) self.clear_secondary_key() while len(self.shrunk_examples) < len(self.interesting_examples): target, example = min( ( (k, v) for k, v in self.interesting_examples.items() if k not in self.shrunk_examples ), key=lambda kv: (sort_key(kv[1].nodes), shortlex(repr(kv[0]))), ) self.debug(f"Shrinking {target!r}: {example.choices}") if not self.settings.report_multiple_bugs: # If multi-bug reporting is disabled, we shrink our currently-minimal # failure, allowing 'slips' to any bug with a smaller minimal example. self.shrink(example, lambda d: d.status == Status.INTERESTING) return def predicate(d: ConjectureResult | _Overrun) -> bool: if d.status < Status.INTERESTING: return False d = cast(ConjectureResult, d) return d.interesting_origin == target self.shrink(example, predicate) self.shrunk_examples.add(target) def clear_secondary_key(self) -> None: if self.has_existing_examples(): # If we have any smaller examples in the secondary corpus, now is # a good time to try them to see if they work as shrinks. They # probably won't, but it's worth a shot and gives us a good # opportunity to clear out the database. # It's not worth trying the primary corpus because we already # tried all of those in the initial phase. corpus = sorted( self.settings.database.fetch(self.secondary_key), key=shortlex ) for c in corpus: choices = choices_from_bytes(c) if choices is None: self.settings.database.delete(self.secondary_key, c) continue primary = { choices_to_bytes(v.choices) for v in self.interesting_examples.values() } if shortlex(c) > max(map(shortlex, primary)): break self.cached_test_function(choices) # We unconditionally remove c from the secondary key as it # is either now primary or worse than our primary example # of this reason for interestingness. self.settings.database.delete(self.secondary_key, c) def shrink( self, example: ConjectureData | ConjectureResult, predicate: ShrinkPredicateT | None = None, allow_transition: ( Callable[[ConjectureData | ConjectureResult, ConjectureData], bool] | None ) = None, ) -> ConjectureData | ConjectureResult: s = self.new_shrinker(example, predicate, allow_transition) s.shrink() return s.shrink_target def new_shrinker( self, example: ConjectureData | ConjectureResult, predicate: ShrinkPredicateT | None = None, allow_transition: ( Callable[[ConjectureData | ConjectureResult, ConjectureData], bool] | None ) = None, ) -> Shrinker: return Shrinker( self, example, predicate, allow_transition=allow_transition, explain=Phase.explain in self.settings.phases, in_target_phase=self._current_phase == "target", ) def passing_choice_sequences( self, prefix: Sequence[ChoiceNode] = () ) -> frozenset[tuple[ChoiceNode, ...]]: """Return a collection of choice sequence nodes which cause the test to pass. Optionally restrict this by a certain prefix, which is useful for explain mode. """ return frozenset( cast(ConjectureResult, result).nodes for key in self.__data_cache if (result := self.__data_cache[key]).status is Status.VALID and startswith(cast(ConjectureResult, result).nodes, prefix) ) class ContainsDiscard(Exception): pass ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/floats.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from array import array from hypothesis.internal.floats import float_to_int, int_to_float """ This module implements support for arbitrary floating point numbers in Conjecture. It doesn't make any attempt to get a good distribution, only to get a format that will shrink well. It works by defining an encoding of non-negative floating point numbers (including NaN values with a zero sign bit) that has good lexical shrinking properties. This encoding is a tagged union of two separate encodings for floating point numbers, with the tag being the first bit of 64 and the remaining 63-bits being the payload. If the tag bit is 0, the next 7 bits are ignored, and the remaining 7 bytes are interpreted as a 7 byte integer in big-endian order and then converted to a float (there is some redundancy here, as 7 * 8 = 56, which is larger than the largest integer that floating point numbers can represent exactly, so multiple encodings may map to the same float). If the tag bit is 1, we instead use something that is closer to the normal representation of floats (and can represent every non-negative float exactly) but has a better ordering: 1. NaNs are ordered after everything else. 2. Infinity is ordered after every finite number. 3. The sign is ignored unless two floating point numbers are identical in absolute magnitude. In that case, the positive is ordered before the negative. 4. Positive floating point numbers are ordered first by int(x) where encoding(x) < encoding(y) if int(x) < int(y). 5. If int(x) == int(y) then x and y are sorted towards lower denominators of their fractional parts. The format of this encoding of floating point goes as follows: [exponent] [mantissa] Each of these is the same size their equivalent in IEEE floating point, but are in a different format. We translate exponents as follows: 1. The maximum exponent (2 ** 11 - 1) is left unchanged. 2. We reorder the remaining exponents so that all of the positive exponents are first, in increasing order, followed by all of the negative exponents in decreasing order (where positive/negative is done by the unbiased exponent e - 1023). We translate the mantissa as follows: 1. If the unbiased exponent is <= 0 we reverse it bitwise. 2. If the unbiased exponent is >= 52 we leave it alone. 3. If the unbiased exponent is in the range [1, 51] then we reverse the low k bits, where k is 52 - unbiased exponent. The low bits correspond to the fractional part of the floating point number. Reversing it bitwise means that we try to minimize the low bits, which kills off the higher powers of 2 in the fraction first. """ MAX_EXPONENT = 0x7FF BIAS = 1023 MAX_POSITIVE_EXPONENT = MAX_EXPONENT - 1 - BIAS def exponent_key(e: int) -> float: if e == MAX_EXPONENT: return float("inf") unbiased = e - BIAS if unbiased < 0: return 10000 - unbiased else: return unbiased ENCODING_TABLE = array("H", sorted(range(MAX_EXPONENT + 1), key=exponent_key)) DECODING_TABLE = array("H", [0]) * len(ENCODING_TABLE) for i, b in enumerate(ENCODING_TABLE): DECODING_TABLE[b] = i del i, b def decode_exponent(e: int) -> int: """Take an integer and turn it into a suitable floating point exponent such that lexicographically simpler leads to simpler floats.""" assert 0 <= e <= MAX_EXPONENT return ENCODING_TABLE[e] def encode_exponent(e: int) -> int: """Take a floating point exponent and turn it back into the equivalent result from conjecture.""" assert 0 <= e <= MAX_EXPONENT return DECODING_TABLE[e] def reverse_byte(b: int) -> int: result = 0 for _ in range(8): result <<= 1 result |= b & 1 b >>= 1 return result # Table mapping individual bytes to the equivalent byte with the bits of the # byte reversed. e.g. 1=0b1 is mapped to 0xb10000000=0x80=128. We use this # precalculated table to simplify calculating the bitwise reversal of a longer # integer. REVERSE_BITS_TABLE = bytearray(map(reverse_byte, range(256))) def reverse64(v: int) -> int: """Reverse a 64-bit integer bitwise. We do this by breaking it up into 8 bytes. The 64-bit integer is then the concatenation of each of these bytes. We reverse it by reversing each byte on its own using the REVERSE_BITS_TABLE above, and then concatenating the reversed bytes. In this case concatenating consists of shifting them into the right position for the word and then oring the bits together. """ assert v.bit_length() <= 64 return ( (REVERSE_BITS_TABLE[(v >> 0) & 0xFF] << 56) | (REVERSE_BITS_TABLE[(v >> 8) & 0xFF] << 48) | (REVERSE_BITS_TABLE[(v >> 16) & 0xFF] << 40) | (REVERSE_BITS_TABLE[(v >> 24) & 0xFF] << 32) | (REVERSE_BITS_TABLE[(v >> 32) & 0xFF] << 24) | (REVERSE_BITS_TABLE[(v >> 40) & 0xFF] << 16) | (REVERSE_BITS_TABLE[(v >> 48) & 0xFF] << 8) | (REVERSE_BITS_TABLE[(v >> 56) & 0xFF] << 0) ) MANTISSA_MASK = (1 << 52) - 1 def reverse_bits(x: int, n: int) -> int: assert x.bit_length() <= n <= 64 x = reverse64(x) x >>= 64 - n return x def update_mantissa(unbiased_exponent: int, mantissa: int) -> int: if unbiased_exponent <= 0: mantissa = reverse_bits(mantissa, 52) elif unbiased_exponent <= 51: n_fractional_bits = 52 - unbiased_exponent fractional_part = mantissa & ((1 << n_fractional_bits) - 1) mantissa ^= fractional_part mantissa |= reverse_bits(fractional_part, n_fractional_bits) return mantissa def lex_to_float(i: int) -> float: assert i.bit_length() <= 64 has_fractional_part = i >> 63 if has_fractional_part: exponent = (i >> 52) & ((1 << 11) - 1) exponent = decode_exponent(exponent) mantissa = i & MANTISSA_MASK mantissa = update_mantissa(exponent - BIAS, mantissa) assert mantissa.bit_length() <= 52 return int_to_float((exponent << 52) | mantissa) else: integral_part = i & ((1 << 56) - 1) return float(integral_part) def float_to_lex(f: float) -> int: if is_simple(f): assert f >= 0 return int(f) return base_float_to_lex(f) def base_float_to_lex(f: float) -> int: i = float_to_int(f) i &= (1 << 63) - 1 exponent = i >> 52 mantissa = i & MANTISSA_MASK mantissa = update_mantissa(exponent - BIAS, mantissa) exponent = encode_exponent(exponent) assert mantissa.bit_length() <= 52 return (1 << 63) | (exponent << 52) | mantissa def is_simple(f: float) -> int: try: i = int(f) except (ValueError, OverflowError): return False if i != f: return False return i.bit_length() <= 56 ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/junkdrawer.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """A module for miscellaneous useful bits and bobs that don't obviously belong anywhere else. If you spot a better home for anything that lives here, please move it.""" import array import gc import itertools import sys import time import warnings from array import ArrayType from collections.abc import Callable, Iterable, Iterator, Sequence from threading import Lock from typing import ( Any, ClassVar, Generic, Literal, TypeVar, Union, overload, ) from sortedcontainers import SortedList from hypothesis.errors import HypothesisWarning T = TypeVar("T") def replace_all( ls: Sequence[T], replacements: Iterable[tuple[int, int, Sequence[T]]], ) -> list[T]: """Substitute multiple replacement values into a list. Replacements is a list of (start, end, value) triples. """ result: list[T] = [] prev = 0 offset = 0 for u, v, r in replacements: result.extend(ls[prev:u]) result.extend(r) prev = v offset += len(r) - (v - u) result.extend(ls[prev:]) assert len(result) == len(ls) + offset return result class IntList(Sequence[int]): """Class for storing a list of non-negative integers compactly. We store them as the smallest size integer array we can get away with. When we try to add an integer that is too large, we upgrade the array to the smallest word size needed to store the new value.""" ARRAY_CODES: ClassVar[list[str]] = ["B", "H", "I", "L", "Q", "O"] NEXT_ARRAY_CODE: ClassVar[dict[str, str]] = dict(itertools.pairwise(ARRAY_CODES)) __slots__ = ("__underlying",) def __init__(self, values: Sequence[int] = ()): for code in self.ARRAY_CODES: try: underlying = self._array_or_list(code, values) break except OverflowError: pass else: # pragma: no cover raise AssertionError(f"Could not create storage for {values!r}") if isinstance(underlying, list): for v in underlying: if not isinstance(v, int) or v < 0: raise ValueError(f"Could not create IntList for {values!r}") self.__underlying: list[int] | ArrayType[int] = underlying @classmethod def of_length(cls, n: int) -> "IntList": return cls(array.array("B", [0]) * n) @staticmethod def _array_or_list( code: str, contents: Iterable[int] ) -> Union[list[int], "ArrayType[int]"]: if code == "O": return list(contents) return array.array(code, contents) def count(self, value: int) -> int: return self.__underlying.count(value) def __repr__(self) -> str: return f"IntList({list(self.__underlying)!r})" def __len__(self) -> int: return len(self.__underlying) @overload def __getitem__(self, i: int) -> int: ... # pragma: no cover @overload def __getitem__( self, i: slice ) -> "list[int] | ArrayType[int]": ... # pragma: no cover def __getitem__(self, i: int | slice) -> "int | list[int] | ArrayType[int]": return self.__underlying[i] def __delitem__(self, i: int | slice) -> None: del self.__underlying[i] def insert(self, i: int, v: int) -> None: self.__underlying.insert(i, v) def __iter__(self) -> Iterator[int]: return iter(self.__underlying) def __eq__(self, other: object) -> bool: if self is other: return True if not isinstance(other, IntList): return NotImplemented return self.__underlying == other.__underlying def __ne__(self, other: object) -> bool: if self is other: return False if not isinstance(other, IntList): return NotImplemented return self.__underlying != other.__underlying def append(self, n: int) -> None: # try the fast path of appending n first. If this overflows, use the # __setitem__ path, which will upgrade the underlying array. try: self.__underlying.append(n) except OverflowError: i = len(self.__underlying) self.__underlying.append(0) self[i] = n def __setitem__(self, i: int, n: int) -> None: while True: try: self.__underlying[i] = n return except OverflowError: assert n > 0 self.__upgrade() def extend(self, ls: Iterable[int]) -> None: for n in ls: self.append(n) def __upgrade(self) -> None: assert isinstance(self.__underlying, array.array) code = self.NEXT_ARRAY_CODE[self.__underlying.typecode] self.__underlying = self._array_or_list(code, self.__underlying) def binary_search(lo: int, hi: int, f: Callable[[int], bool]) -> int: """Binary searches in [lo , hi) to find n such that f(n) == f(lo) but f(n + 1) != f(lo). It is implicitly assumed and will not be checked that f(hi) != f(lo). """ reference = f(lo) while lo + 1 < hi: mid = (lo + hi) // 2 if f(mid) == reference: lo = mid else: hi = mid return lo class LazySequenceCopy(Generic[T]): """A "copy" of a sequence that works by inserting a mask in front of the underlying sequence, so that you can mutate it without changing the underlying sequence. Effectively behaves as if you could do list(x) in O(1) time. The full list API is not supported yet but there's no reason in principle it couldn't be.""" def __init__(self, values: Sequence[T]): self.__values = values self.__len = len(values) self.__mask: dict[int, T] | None = None self.__popped_indices: SortedList[int] | None = None def __len__(self) -> int: if self.__popped_indices is None: return self.__len return self.__len - len(self.__popped_indices) def pop(self, i: int = -1) -> T: if len(self) == 0: raise IndexError("Cannot pop from empty list") i = self.__underlying_index(i) v = None if self.__mask is not None: v = self.__mask.pop(i, None) if v is None: v = self.__values[i] if self.__popped_indices is None: self.__popped_indices = SortedList() self.__popped_indices.add(i) return v def swap(self, i: int, j: int) -> None: """Swap the elements ls[i], ls[j].""" if i == j: return self[i], self[j] = self[j], self[i] def __getitem__(self, i: int) -> T: i = self.__underlying_index(i) default = self.__values[i] if self.__mask is None: return default else: return self.__mask.get(i, default) def __setitem__(self, i: int, v: T) -> None: i = self.__underlying_index(i) if self.__mask is None: self.__mask = {} self.__mask[i] = v def __underlying_index(self, i: int) -> int: n = len(self) if i < -n or i >= n: raise IndexError(f"Index {i} out of range [0, {n})") if i < 0: i += n assert 0 <= i < n if self.__popped_indices is not None: # given an index i in the popped representation of the list, compute # its corresponding index in the underlying list. given # l = [1, 4, 2, 10, 188] # l.pop(3) # l.pop(1) # assert l == [1, 2, 188] # # we want l[i] == self.__values[f(i)], where f is this function. assert len(self.__popped_indices) <= len(self.__values) for idx in self.__popped_indices: if idx > i: break i += 1 return i # even though we have len + getitem, mypyc requires iter. def __iter__(self) -> Iterable[T]: for i in range(len(self)): yield self[i] def stack_depth_of_caller() -> int: """Get stack size for caller's frame. From https://stackoverflow.com/a/47956089/9297601 , this is a simple but much faster alternative to `len(inspect.stack(0))`. We use it with get/set recursionlimit to make stack overflows non-flaky; see https://github.com/HypothesisWorks/hypothesis/issues/2494 for details. """ frame = sys._getframe(2) size = 1 while frame: frame = frame.f_back # type: ignore[assignment] size += 1 return size class StackframeLimiter: # StackframeLimiter is used to make the recursion limit warning issued via # ensure_free_stackframes thread-safe. We track the known values we have # passed to sys.setrecursionlimit in _known_limits, and only issue a warning # if sys.getrecursionlimit is not in _known_limits. # # This will always be an under-approximation of when we would ideally issue # this warning, since a non-hypothesis caller could coincidentaly set the # recursion limit to one of our known limits. Currently, StackframeLimiter # resets _known_limits whenever all of the ensure_free_stackframes contexts # have exited. We could increase the power of the warning by tracking a # refcount for each limit, and removing it as soon as the refcount hits zero. # I didn't think this extra complexity is worth the minor power increase for # what is already only a "nice to have" warning. def __init__(self): self._active_contexts = 0 self._known_limits: set[int] = set() self._original_limit: int | None = None def _setrecursionlimit(self, new_limit: int, *, check: bool = True) -> None: if ( check and (current_limit := sys.getrecursionlimit()) not in self._known_limits ): warnings.warn( "The recursion limit will not be reset, since it was changed " f"during test execution (from {self._original_limit} to {current_limit}).", HypothesisWarning, stacklevel=4, ) return self._known_limits.add(new_limit) sys.setrecursionlimit(new_limit) def enter_context(self, new_limit: int, *, current_limit: int) -> None: if self._active_contexts == 0: # this is the first context on the stack. Record the true original # limit, to restore later. assert self._original_limit is None self._original_limit = current_limit self._known_limits.add(self._original_limit) self._active_contexts += 1 self._setrecursionlimit(new_limit) def exit_context(self, new_limit: int, *, check: bool = True) -> None: assert self._active_contexts > 0 self._active_contexts -= 1 if self._active_contexts == 0: # this is the last context to exit. Restore the true original # limit and clear our known limits. original_limit = self._original_limit assert original_limit is not None try: self._setrecursionlimit(original_limit, check=check) finally: self._original_limit = None # we want to clear the known limits, but preserve the limit # we just set it to as known. self._known_limits = {original_limit} else: self._setrecursionlimit(new_limit, check=check) _stackframe_limiter = StackframeLimiter() _stackframe_limiter_lock = Lock() class ensure_free_stackframes: """Context manager that ensures there are at least N free stackframes (for a reasonable value of N). """ def __enter__(self) -> None: cur_depth = stack_depth_of_caller() with _stackframe_limiter_lock: self.old_limit = sys.getrecursionlimit() # The default CPython recursionlimit is 1000, but pytest seems to bump # it to 3000 during test execution. Let's make it something reasonable: self.new_limit = cur_depth + 2000 # Because we add to the recursion limit, to be good citizens we also # add a check for unbounded recursion. The default limit is typically # 1000/3000, so this can only ever trigger if something really strange # is happening and it's hard to imagine an # intentionally-deeply-recursive use of this code. assert cur_depth <= 1000, ( "Hypothesis would usually add %d to the stack depth of %d here, " "but we are already much deeper than expected. Aborting now, to " "avoid extending the stack limit in an infinite loop..." % (self.new_limit - self.old_limit, self.old_limit) ) try: _stackframe_limiter.enter_context( self.new_limit, current_limit=self.old_limit ) except Exception: # if the stackframe limiter raises a HypothesisWarning (under eg # -Werror), __exit__ is not called, since we errored in __enter__. # Preserve the state of the stackframe limiter by exiting, and # avoid showing a duplicate warning with check=False. _stackframe_limiter.exit_context(self.old_limit, check=False) raise def __exit__(self, *args, **kwargs): with _stackframe_limiter_lock: _stackframe_limiter.exit_context(self.old_limit) def find_integer(f: Callable[[int], bool]) -> int: """Finds a (hopefully large) integer such that f(n) is True and f(n + 1) is False. f(0) is assumed to be True and will not be checked. """ # We first do a linear scan over the small numbers and only start to do # anything intelligent if f(4) is true. This is because it's very hard to # win big when the result is small. If the result is 0 and we try 2 first # then we've done twice as much work as we needed to! for i in range(1, 5): if not f(i): return i - 1 # We now know that f(4) is true. We want to find some number for which # f(n) is *not* true. # lo is the largest number for which we know that f(lo) is true. lo = 4 # Exponential probe upwards until we find some value hi such that f(hi) # is not true. Subsequently we maintain the invariant that hi is the # smallest number for which we know that f(hi) is not true. hi = 5 while f(hi): lo = hi hi *= 2 # Now binary search until lo + 1 = hi. At that point we have f(lo) and not # f(lo + 1), as desired.. while lo + 1 < hi: mid = (lo + hi) // 2 if f(mid): lo = mid else: hi = mid return lo _gc_initialized = False _gc_start: float = 0 _gc_cumulative_time: float = 0 # Since gc_callback potentially runs in test context, and perf_counter # might be monkeypatched, we store a reference to the real one. _perf_counter = time.perf_counter def gc_cumulative_time() -> float: global _gc_initialized # I don't believe we need a lock for the _gc_cumulative_time increment here, # since afaik each gc callback is only executed once when the garbage collector # runs, by the thread which initiated the gc. if not _gc_initialized: if hasattr(gc, "callbacks"): # CPython def gc_callback( phase: Literal["start", "stop"], info: dict[str, int] ) -> None: global _gc_start, _gc_cumulative_time try: now = _perf_counter() if phase == "start": _gc_start = now elif phase == "stop" and _gc_start > 0: _gc_cumulative_time += now - _gc_start # pragma: no cover # ?? except RecursionError: # pragma: no cover # Avoid flakiness via UnraisableException, which is caught and # warned by pytest. The actual callback (this function) is # validated to never trigger a RecursionError itself when # when called by gc.collect. # Anyway, we should hit the same error on "start" # and "stop", but to ensure we don't get out of sync we just # signal that there is no matching start. _gc_start = 0 return gc.callbacks.insert(0, gc_callback) elif hasattr(gc, "hooks"): # pragma: no cover # pypy only # PyPy def hook(stats: Any) -> None: global _gc_cumulative_time try: _gc_cumulative_time += stats.duration except RecursionError: pass if gc.hooks.on_gc_minor is None: gc.hooks.on_gc_minor = hook if gc.hooks.on_gc_collect_step is None: gc.hooks.on_gc_collect_step = hook _gc_initialized = True return _gc_cumulative_time def startswith(l1: Sequence[T], l2: Sequence[T]) -> bool: if len(l1) < len(l2): return False return all(v1 == v2 for v1, v2 in zip(l1[: len(l2)], l2, strict=False)) def endswith(l1: Sequence[T], l2: Sequence[T]) -> bool: if len(l1) < len(l2): return False return all(v1 == v2 for v1, v2 in zip(l1[-len(l2) :], l2, strict=False)) def bits_to_bytes(n: int) -> int: """The number of bytes required to represent an n-bit number. Equivalent to (n + 7) // 8, but slightly faster. This really is called enough times that that matters.""" return (n + 7) >> 3 ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.internal.compat import int_from_bytes, int_to_bytes from hypothesis.internal.conjecture.choice import ChoiceT, choice_permitted from hypothesis.internal.conjecture.data import ConjectureResult, Status, _Overrun from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.conjecture.junkdrawer import bits_to_bytes, find_integer from hypothesis.internal.conjecture.pareto import NO_SCORE class Optimiser: """A fairly basic optimiser designed to increase the value of scores for targeted property-based testing. This implements a fairly naive hill climbing algorithm based on randomly regenerating parts of the test case to attempt to improve the result. It is not expected to produce amazing results, because it is designed to be run in a fairly small testing budget, so it prioritises finding easy wins and bailing out quickly if that doesn't work. For more information about targeted property-based testing, see Löscher, Andreas, and Konstantinos Sagonas. "Targeted property-based testing." Proceedings of the 26th ACM SIGSOFT International Symposium on Software Testing and Analysis. ACM, 2017. """ def __init__( self, engine: ConjectureRunner, data: ConjectureResult, target: str, max_improvements: int = 100, ) -> None: """Optimise ``target`` starting from ``data``. Will stop either when we seem to have found a local maximum or when the target score has been improved ``max_improvements`` times. This limit is in place to deal with the fact that the target score may not be bounded above.""" self.engine = engine self.current_data = data self.target = target self.max_improvements = max_improvements self.improvements = 0 def run(self) -> None: self.hill_climb() def score_function(self, data: ConjectureResult) -> float: return data.target_observations.get(self.target, NO_SCORE) @property def current_score(self) -> float: return self.score_function(self.current_data) def consider_new_data(self, data: ConjectureResult | _Overrun) -> bool: """Consider a new data object as a candidate target. If it is better than the current one, return True.""" if data.status < Status.VALID: return False assert isinstance(data, ConjectureResult) score = self.score_function(data) if score < self.current_score: return False if score > self.current_score: self.improvements += 1 self.current_data = data return True assert score == self.current_score # We allow transitions that leave the score unchanged as long as they # don't increase the number of nodes. This gives us a certain amount of # freedom for lateral moves that will take us out of local maxima. if len(data.nodes) <= len(self.current_data.nodes): self.current_data = data return True return False def hill_climb(self) -> None: """The main hill climbing loop where we actually do the work: Take data, and attempt to improve its score for target. select_example takes a data object and returns an index to an example where we should focus our efforts.""" nodes_examined = set() prev: ConjectureResult | None = None i = len(self.current_data.nodes) - 1 while i >= 0 and self.improvements <= self.max_improvements: if prev is not self.current_data: i = len(self.current_data.nodes) - 1 prev = self.current_data if i in nodes_examined: i -= 1 continue nodes_examined.add(i) node = self.current_data.nodes[i] assert node.index is not None # we can only (sensibly & easily) define hill climbing for # numeric-style nodes. It's not clear hill-climbing a string is # useful, for instance. if node.type not in {"integer", "float", "bytes", "boolean"}: continue def attempt_replace(k: int) -> bool: """ Try replacing the current node in the current best test case with a value which is "k times larger", where the exact notion of "larger" depends on the choice_type. Note that we use the *current* best and not the one we started with. This helps ensure that if we luck into a good draw when making random choices we get to keep the good bits. """ # we don't want to infinitely drive up an unbounded score. if abs(k) > 2**20: return False node = self.current_data.nodes[i] assert node.index is not None if node.was_forced: return False # pragma: no cover new_choice: ChoiceT if node.type in {"integer", "float"}: assert isinstance(node.value, (int, float)) new_choice = node.value + k elif node.type == "boolean": assert isinstance(node.value, bool) if abs(k) > 1: return False if k == -1: new_choice = False if k == 1: new_choice = True if k == 0: # pragma: no cover new_choice = node.value else: assert node.type == "bytes" assert isinstance(node.value, bytes) v = int_from_bytes(node.value) # can't go below zero for bytes if v + k < 0: return False v += k # allow adding k to increase the number of bytes. we don't want # to decrease so that b"01" doesn't turn into b"1". size = max(len(node.value), bits_to_bytes(v.bit_length())) new_choice = int_to_bytes(v, size) if not choice_permitted(new_choice, node.constraints): return False for _ in range(3): choices = self.current_data.choices attempt_choices = ( choices[: node.index] + (new_choice,) + choices[node.index + 1 :] ) attempt = self.engine.cached_test_function( attempt_choices, extend="full" ) if self.consider_new_data(attempt): return True if attempt.status is Status.OVERRUN: return False assert isinstance(attempt, ConjectureResult) if len(attempt.nodes) == len(self.current_data.nodes): return False for j, ex in enumerate(self.current_data.spans): if ex.start >= node.index + 1: break # pragma: no cover if ex.end <= node.index: continue ex_attempt = attempt.spans[j] if ex.choice_count == ex_attempt.choice_count: continue # pragma: no cover replacement = attempt.choices[ex_attempt.start : ex_attempt.end] if self.consider_new_data( self.engine.cached_test_function( choices[: node.index] + replacement + self.current_data.choices[ex.end :] ) ): return True return False # we don't know whether a target score increases or decreases with # respect to the value of some node, so try both directions. find_integer(lambda k: attempt_replace(k)) find_integer(lambda k: attempt_replace(-k)) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/pareto.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Callable, Iterator from enum import Enum from random import Random from typing import TYPE_CHECKING from sortedcontainers import SortedList from hypothesis.internal.conjecture.choice import choices_key from hypothesis.internal.conjecture.data import ( ConjectureData, ConjectureResult, Status, _Overrun, ) from hypothesis.internal.conjecture.junkdrawer import LazySequenceCopy from hypothesis.internal.conjecture.shrinker import sort_key NO_SCORE = float("-inf") if TYPE_CHECKING: from hypothesis.internal.conjecture.engine import ConjectureRunner class DominanceRelation(Enum): NO_DOMINANCE = 0 EQUAL = 1 LEFT_DOMINATES = 2 RIGHT_DOMINATES = 3 def dominance(left: ConjectureResult, right: ConjectureResult) -> DominanceRelation: """Returns the dominance relation between ``left`` and ``right``, according to the rules that one ConjectureResult dominates another if and only if it is better in every way. The things we currently consider to be "better" are: * Something that is smaller in shrinking order is better. * Something that has higher status is better. * Each ``interesting_origin`` is treated as its own score, so if two interesting examples have different origins then neither dominates the other. * For each target observation, a higher score is better. In "normal" operation where there are no bugs or target observations, the pareto front only has one element (the smallest valid test case), but for more structured or failing tests it can be useful to track, and future work will depend on it more.""" left_key = sort_key(left.nodes) right_key = sort_key(right.nodes) if left_key == right_key: return DominanceRelation.EQUAL if right_key < left_key: result = dominance(left=right, right=left) if result == DominanceRelation.LEFT_DOMINATES: return DominanceRelation.RIGHT_DOMINATES else: # Because we have sort_key(left) < sort_key(right) the only options # are that right is better than left or that the two are # incomparable. assert result == DominanceRelation.NO_DOMINANCE return result # Either left is better or there is no dominance relationship. assert left_key < right_key # The right is more interesting if left.status < right.status: return DominanceRelation.NO_DOMINANCE if not right.tags.issubset(left.tags): return DominanceRelation.NO_DOMINANCE # Things that are interesting for different reasons are incomparable in # the dominance relationship. if ( left.status == Status.INTERESTING and right.interesting_origin is not None and left.interesting_origin != right.interesting_origin ): return DominanceRelation.NO_DOMINANCE for target in set(left.target_observations) | set(right.target_observations): left_score = left.target_observations.get(target, NO_SCORE) right_score = right.target_observations.get(target, NO_SCORE) if right_score > left_score: return DominanceRelation.NO_DOMINANCE return DominanceRelation.LEFT_DOMINATES class ParetoFront: """Maintains an approximate pareto front of ConjectureData objects. That is, we try to maintain a collection of objects such that no element of the collection is pareto dominated by any other. In practice we don't quite manage that, because doing so is computationally very expensive. Instead we maintain a random sample of data objects that are "rarely" dominated by any other element of the collection (roughly, no more than about 10%). Only valid test cases are considered to belong to the pareto front - any test case with a status less than valid is discarded. Note that the pareto front is potentially quite large, and currently this will store the entire front in memory. This is bounded by the number of valid examples we run, which is max_examples in normal execution, and currently we do not support workflows with large max_examples which have large values of max_examples very well anyway, so this isn't a major issue. In future we may weish to implement some sort of paging out to disk so that we can work with larger fronts. Additionally, because this is only an approximate pareto front, there are scenarios where it can be much larger than the actual pareto front. There isn't a huge amount we can do about this - checking an exact pareto front is intrinsically quadratic. "Most" of the time we should be relatively close to the true pareto front, say within an order of magnitude, but it's not hard to construct scenarios where this is not the case. e.g. suppose we enumerate all valid test cases in increasing shortlex order as s_1, ..., s_n, ... and have scores f and g such that f(s_i) = min(i, N) and g(s_i) = 1 if i >= N, then the pareto front is the set {s_1, ..., S_N}, but the only element of the front that will dominate s_i when i > N is S_N, which we select with probability 1 / N. A better data structure could solve this, but at the cost of more expensive operations and higher per element memory use, so we'll wait to see how much of a problem this is in practice before we try that. """ def __init__(self, random: Random) -> None: self.__random = random self.__eviction_listeners: list[Callable[[ConjectureResult], None]] = [] self.front: SortedList[ConjectureResult] = SortedList( key=lambda d: sort_key(d.nodes) ) self.__pending: ConjectureResult | None = None def add(self, data: ConjectureData | ConjectureResult | _Overrun) -> bool: """Attempts to add ``data`` to the pareto front. Returns True if ``data`` is now in the front, including if data is already in the collection, and False otherwise""" if data.status < Status.VALID: return False assert not isinstance(data, _Overrun) data = data.as_result() assert not isinstance(data, _Overrun) if not self.front: self.front.add(data) return True if data in self.front: return True # We add data to the pareto front by adding it unconditionally and then # doing a certain amount of randomized "clear down" - testing a random # set of elements (currently 10) to see if they are dominated by # something else in the collection. If they are, we remove them. self.front.add(data) assert self.__pending is None try: self.__pending = data # We maintain a set of the current exact pareto front of the # values we've sampled so far. When we sample a new element we # either add it to this exact pareto front or remove it from the # collection entirely. front = LazySequenceCopy(self.front) # We track which values we are going to remove and remove them all # at the end so the shape of the front doesn't change while we're # using it. to_remove: list[ConjectureResult] = [] # We now iteratively sample elements from the approximate pareto # front to check whether they should be retained. When the set of # dominators gets too large we have sampled at least 10 elements # and it gets too expensive to continue, so we consider that enough # due diligence. i = self.front.index(data) # First we attempt to look for values that must be removed by the # addition of the data. These are necessarily to the right of it # in the list. failures = 0 while i + 1 < len(front) and failures < 10: j = self.__random.randrange(i + 1, len(front)) candidate = front.pop(j) dom = dominance(data, candidate) assert dom != DominanceRelation.RIGHT_DOMINATES if dom == DominanceRelation.LEFT_DOMINATES: to_remove.append(candidate) failures = 0 else: failures += 1 # Now we look at the points up to where we put data in to see if # it is dominated. While we're here we spend some time looking for # anything else that might be dominated too, compacting down parts # of the list. dominators = [data] while i >= 0 and len(dominators) < 10: front.swap(i, self.__random.randint(0, i)) candidate = front[i] already_replaced = False j = 0 while j < len(dominators): v = dominators[j] dom = dominance(candidate, v) if dom == DominanceRelation.LEFT_DOMINATES: if not already_replaced: already_replaced = True dominators[j] = candidate j += 1 else: # pragma: no cover # flaky, by test_database_contains_only_pareto_front dominators[j], dominators[-1] = ( dominators[-1], dominators[j], ) dominators.pop() to_remove.append(v) elif dom == DominanceRelation.RIGHT_DOMINATES: to_remove.append(candidate) break elif dom == DominanceRelation.EQUAL: break else: j += 1 else: dominators.append(candidate) i -= 1 for v in to_remove: self._remove(v) return data in self.front finally: self.__pending = None def on_evict(self, f: Callable[[ConjectureResult], None]) -> None: """Register a listener function that will be called with data when it gets removed from the front because something else dominates it.""" self.__eviction_listeners.append(f) def __contains__(self, data: object) -> bool: if not isinstance(data, (ConjectureData, ConjectureResult)): return False result = data.as_result() if isinstance(result, _Overrun): return False return result in self.front def __iter__(self) -> Iterator[ConjectureResult]: return iter(self.front) def __getitem__(self, i: int) -> ConjectureResult: return self.front[i] def __len__(self) -> int: return len(self.front) def _remove(self, data: ConjectureResult) -> None: try: self.front.remove(data) except ValueError: return if data is not self.__pending: for f in self.__eviction_listeners: f(data) class ParetoOptimiser: """Class for managing optimisation of the pareto front. That is, given the current best known pareto front, this class runs an optimisation process that attempts to bring it closer to the actual pareto front. Currently this is fairly basic and only handles pareto optimisation that works by reducing the test case in the shortlex order. We expect it will grow more powerful over time. """ def __init__(self, engine: "ConjectureRunner") -> None: self.__engine = engine assert self.__engine.pareto_front is not None self.front: ParetoFront = self.__engine.pareto_front def run(self) -> None: seen = set() # We iterate backwards through the pareto front, using the shrinker to # (hopefully) replace each example with a smaller one. Note that it's # important that we start from the end for two reasons: Firstly, by # doing it this way we ensure that any new front members we discover # during optimisation will also get optimised (because they will be # inserted into the part of the front that we haven't visited yet), # and secondly we generally expect that we will not finish this process # in a single run, because it's relatively expensive in terms of our # example budget, and by starting from the end we ensure that each time # we run the tests we improve the pareto front because we work on the # bits that we haven't covered yet. i = len(self.front) - 1 prev = None while i >= 0 and not self.__engine.interesting_examples: assert self.front i = min(i, len(self.front) - 1) target = self.front[i] if choices_key(target.choices) in seen: i -= 1 continue assert target is not prev prev = target def allow_transition(source, destination): """Shrink to data that strictly pareto dominates the current best value we've seen, which is the current target of the shrinker. Note that during shrinking we may discover other smaller examples that this function will reject and will get added to the front. This is fine, because they will be processed on later iterations of this loop.""" if dominance(destination, source) == DominanceRelation.LEFT_DOMINATES: # If ``destination`` dominates ``source`` then ``source`` # must be dominated in the front - either ``destination`` is in # the front, or it was not added to it because it was # dominated by something in it. self.front._remove(source) return True return False shrunk = self.__engine.shrink(target, allow_transition=allow_transition) seen.add(choices_key(shrunk.choices)) # Note that the front may have changed shape arbitrarily when # we ran the shrinker. If it didn't change shape then this is # i - 1. If it did change shape then this is the largest value # in the front which is smaller than the previous target, so # is the correct place to resume from. In particular note that the # size of the front might have grown because of slippage during the # shrink, but all of the newly introduced elements will be smaller # than `target`, so will be covered by this iteration. i = self.front.front.bisect_left(target) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/provider_conformance.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys from collections.abc import Collection, Iterable, Sequence from typing import Any from hypothesis import ( HealthCheck, assume, note, settings as Settings, strategies as st, ) from hypothesis.errors import BackendCannotProceed from hypothesis.internal.compat import batched from hypothesis.internal.conjecture.choice import ( ChoiceTypeT, choice_permitted, ) from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.providers import ( COLLECTION_DEFAULT_MAX_SIZE, HypothesisProvider, PrimitiveProvider, with_register_backend, ) from hypothesis.internal.floats import SMALLEST_SUBNORMAL, sign_aware_lte from hypothesis.internal.intervalsets import IntervalSet from hypothesis.stateful import RuleBasedStateMachine, initialize, precondition, rule from hypothesis.strategies import DrawFn, SearchStrategy from hypothesis.strategies._internal.strings import OneCharStringStrategy, TextStrategy def build_intervals(intervals: list[int]) -> list[tuple[int, int]]: if len(intervals) % 2: intervals = intervals[:-1] intervals.sort() # help mypy infer tuple[int, ...] -> tuple[int, int] return list(batched(intervals, 2, strict=True)) # type: ignore def interval_lists( *, min_codepoint: int = 0, max_codepoint: int = sys.maxunicode, min_size: int = 0 ) -> SearchStrategy[Iterable[Sequence[int]]]: return ( st.lists( st.integers(min_codepoint, max_codepoint), unique=True, min_size=min_size * 2, ) .map(sorted) .map(build_intervals) ) def intervals( *, min_codepoint: int = 0, max_codepoint: int = sys.maxunicode, min_size: int = 0 ) -> SearchStrategy[IntervalSet]: return st.builds( IntervalSet, interval_lists( min_codepoint=min_codepoint, max_codepoint=max_codepoint, min_size=min_size ), ) @st.composite def integer_weights( draw: DrawFn, min_value: int | None = None, max_value: int | None = None ) -> dict[int, float]: # Sampler doesn't play well with super small floats, so exclude them weights = draw( st.dictionaries( st.integers(min_value=min_value, max_value=max_value), st.floats(0.001, 1), min_size=1, max_size=255, ) ) # invalid to have a weighting that disallows all possibilities assume(sum(weights.values()) != 0) # re-normalize probabilities to sum to some arbitrary target < 1 target = draw(st.floats(0.001, 0.999)) factor = target / sum(weights.values()) weights = {k: v * factor for k, v in weights.items()} # float rounding error can cause this to fail. assume(0.001 <= sum(weights.values()) <= 0.999) return weights @st.composite def integer_constraints( draw, *, use_min_value=None, use_max_value=None, use_shrink_towards=None, use_weights=None, use_forced=False, ): min_value = None max_value = None shrink_towards = 0 weights = None if use_min_value is None: use_min_value = draw(st.booleans()) if use_max_value is None: use_max_value = draw(st.booleans()) use_shrink_towards = draw(st.booleans()) if use_weights is None: use_weights = ( draw(st.booleans()) if (use_min_value and use_max_value) else False ) # Invariants: # (1) min_value <= forced <= max_value # (2) sum(weights.values()) < 1 # (3) len(weights) <= 255 if use_shrink_towards: shrink_towards = draw(st.integers()) forced = draw(st.integers()) if use_forced else None if use_weights: assert use_max_value assert use_min_value min_value = draw(st.integers(max_value=forced)) min_val = max(min_value, forced) if forced is not None else min_value max_value = draw(st.integers(min_value=min_val)) weights = draw(integer_weights(min_value, max_value)) else: if use_min_value: min_value = draw(st.integers(max_value=forced)) if use_max_value: min_vals = [] if min_value is not None: min_vals.append(min_value) if forced is not None: min_vals.append(forced) min_val = max(min_vals) if min_vals else None max_value = draw(st.integers(min_value=min_val)) if forced is not None: assume((forced - shrink_towards).bit_length() < 128) return { "min_value": min_value, "max_value": max_value, "shrink_towards": shrink_towards, "weights": weights, "forced": forced, } @st.composite def _collection_constraints( draw: DrawFn, *, forced: Any | None, use_min_size: bool | None = None, use_max_size: bool | None = None, ) -> dict[str, int]: min_size = 0 max_size = COLLECTION_DEFAULT_MAX_SIZE # collections are quite expensive in entropy. cap to avoid overruns. cap = 50 if use_min_size is None: use_min_size = draw(st.booleans()) if use_max_size is None: use_max_size = draw(st.booleans()) if use_min_size: min_size = draw( st.integers(0, min(len(forced), cap) if forced is not None else cap) ) if use_max_size: max_size = draw( st.integers( min_value=min_size if forced is None else max(min_size, len(forced)) ) ) if forced is None: # cap to some reasonable max size to avoid overruns. max_size = min(max_size, min_size + 100) return {"min_size": min_size, "max_size": max_size} @st.composite def string_constraints( draw: DrawFn, *, use_min_size: bool | None = None, use_max_size: bool | None = None, use_forced: bool = False, ) -> Any: interval_set = draw(intervals()) forced = ( draw(TextStrategy(OneCharStringStrategy(interval_set))) if use_forced else None ) constraints = draw( _collection_constraints( forced=forced, use_min_size=use_min_size, use_max_size=use_max_size ) ) # if the intervalset is empty, then the min size must be zero, because the # only valid value is the empty string. if len(interval_set) == 0: constraints["min_size"] = 0 return {"intervals": interval_set, "forced": forced, **constraints} @st.composite def bytes_constraints( draw: DrawFn, *, use_min_size: bool | None = None, use_max_size: bool | None = None, use_forced: bool = False, ) -> Any: forced = draw(st.binary()) if use_forced else None constraints = draw( _collection_constraints( forced=forced, use_min_size=use_min_size, use_max_size=use_max_size ) ) return {"forced": forced, **constraints} @st.composite def float_constraints( draw, *, use_min_value=None, use_max_value=None, use_forced=False, ): if use_min_value is None: use_min_value = draw(st.booleans()) if use_max_value is None: use_max_value = draw(st.booleans()) forced = draw(st.floats()) if use_forced else None pivot = forced if (use_forced and not math.isnan(forced)) else None min_value = -math.inf max_value = math.inf smallest_nonzero_magnitude = SMALLEST_SUBNORMAL allow_nan = True if (use_forced and math.isnan(forced)) else draw(st.booleans()) if use_min_value: min_value = draw(st.floats(max_value=pivot, allow_nan=False)) if use_max_value: if pivot is None: min_val = min_value else: min_val = pivot if sign_aware_lte(min_value, pivot) else min_value max_value = draw(st.floats(min_value=min_val, allow_nan=False)) largest_magnitude = max(abs(min_value), abs(max_value)) # can't force something smaller than our smallest magnitude. if pivot is not None and pivot != 0.0: largest_magnitude = min(largest_magnitude, pivot) # avoid drawing from an empty range if largest_magnitude > 0: smallest_nonzero_magnitude = draw( st.floats( min_value=0, # smallest_nonzero_magnitude breaks internal clamper invariants if # it is allowed to be larger than the magnitude of {min, max}_value. # # Let's also be reasonable here; smallest_nonzero_magnitude is used # for subnormals, so we will never provide a number above 1 in practice. max_value=min(largest_magnitude, 1.0), exclude_min=True, ) ) assert sign_aware_lte(min_value, max_value) return { "min_value": min_value, "max_value": max_value, "forced": forced, "allow_nan": allow_nan, "smallest_nonzero_magnitude": smallest_nonzero_magnitude, } @st.composite def boolean_constraints(draw: DrawFn, *, use_forced: bool = False) -> Any: forced = draw(st.booleans()) if use_forced else None # avoid invalid forced combinations p = draw(st.floats(0, 1, exclude_min=forced is True, exclude_max=forced is False)) return {"p": p, "forced": forced} def constraints_strategy(choice_type, strategy_constraints=None, *, use_forced=False): strategy = { "boolean": boolean_constraints, "integer": integer_constraints, "float": float_constraints, "bytes": bytes_constraints, "string": string_constraints, }[choice_type] if strategy_constraints is None: strategy_constraints = {} return strategy(**strategy_constraints.get(choice_type, {}), use_forced=use_forced) def choice_types_constraints(strategy_constraints=None, *, use_forced=False): options: list[ChoiceTypeT] = ["boolean", "integer", "float", "bytes", "string"] return st.one_of( st.tuples( st.just(name), constraints_strategy(name, strategy_constraints, use_forced=use_forced), ) for name in options ) def run_conformance_test( Provider: type[PrimitiveProvider], *, context_manager_exceptions: Collection[type[BaseException]] = (), settings: Settings | None = None, _realize_objects: SearchStrategy[Any] = ( st.from_type(object) | st.from_type(type).flatmap(st.from_type) ), ) -> None: """ Test that the given ``Provider`` class conforms to the |PrimitiveProvider| interface. For instance, this tests that ``Provider`` does not return out of bounds choices from any of the ``draw_*`` methods, or violate other invariants which Hypothesis depends on. This function is intended to be called at test-time, not at runtime. It is provided by Hypothesis to make it easy for third-party backend authors to test their provider. Backend authors wishing to test their provider should include a test similar to the following in their test suite: .. code-block:: python from hypothesis.internal.conjecture.provider_conformance import run_conformance_test def test_conformance(): run_conformance_test(MyProvider) If your provider can raise control flow exceptions inside one of the five ``draw_*`` methods that are handled by your provider's ``per_test_case_context_manager``, pass a list of these exceptions types to ``context_manager_exceptions``. Otherwise, ``run_conformance_test`` will treat those exceptions as fatal errors. """ class CopiesRealizationProvider(HypothesisProvider): avoid_realization = Provider.avoid_realization with with_register_backend("copies_realization", CopiesRealizationProvider): @Settings( settings, suppress_health_check=[HealthCheck.too_slow], backend="copies_realization", ) class ProviderConformanceTest(RuleBasedStateMachine): def __init__(self): super().__init__() @initialize(random=st.randoms()) def setup(self, random): if Provider.lifetime == "test_case": data = ConjectureData(random=random, provider=Provider) self.provider = data.provider else: self.provider = Provider(None) self.context_manager = self.provider.per_test_case_context_manager() self.context_manager.__enter__() self.frozen = False def _draw(self, choice_type, constraints): del constraints["forced"] draw_func = getattr(self.provider, f"draw_{choice_type}") try: choice = draw_func(**constraints) note(f"drew {choice_type} {choice}") expected_type = { "integer": int, "float": float, "bytes": bytes, "string": str, "boolean": bool, }[choice_type] assert isinstance(choice, expected_type) assert choice_permitted(choice, constraints) except context_manager_exceptions as e: note( f"caught exception {type(e)} in context_manager_exceptions: {e}" ) try: self.context_manager.__exit__(type(e), e, None) except BackendCannotProceed: self.frozen = True return None return choice @precondition(lambda self: not self.frozen) @rule(constraints=integer_constraints()) def draw_integer(self, constraints): self._draw("integer", constraints) @precondition(lambda self: not self.frozen) @rule(constraints=float_constraints()) def draw_float(self, constraints): self._draw("float", constraints) @precondition(lambda self: not self.frozen) @rule(constraints=bytes_constraints()) def draw_bytes(self, constraints): self._draw("bytes", constraints) @precondition(lambda self: not self.frozen) @rule(constraints=string_constraints()) def draw_string(self, constraints): self._draw("string", constraints) @precondition(lambda self: not self.frozen) @rule(constraints=boolean_constraints()) def draw_boolean(self, constraints): self._draw("boolean", constraints) @precondition(lambda self: not self.frozen) @rule(label=st.integers()) def span_start(self, label): self.provider.span_start(label) @precondition(lambda self: not self.frozen) @rule(discard=st.booleans()) def span_end(self, discard): self.provider.span_end(discard) @precondition(lambda self: not self.frozen) @rule() def freeze(self): # phase-transition, mimicking data.freeze() at the end of a test case. self.frozen = True self.context_manager.__exit__(None, None, None) @precondition(lambda self: self.frozen) @rule(value=_realize_objects) def realize(self, value): # filter out nans and weirder things try: assume(value == value) except Exception: # e.g. value = Decimal('-sNaN') assume(False) # if `value` is non-symbolic, the provider should return it as-is. assert self.provider.realize(value) == value @precondition(lambda self: self.frozen) @rule() def observe_test_case(self): observations = self.provider.observe_test_case() assert isinstance(observations, dict) @precondition(lambda self: self.frozen) @rule(lifetime=st.sampled_from(["test_function", "test_case"])) def observe_information_messages(self, lifetime): observations = self.provider.observe_information_messages( lifetime=lifetime ) for observation in observations: assert isinstance(observation, dict) def teardown(self): if not self.frozen: self.context_manager.__exit__(None, None, None) ProviderConformanceTest.TestCase().runTest() ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/providers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import abc import contextlib import math import sys import warnings from collections.abc import Iterable from contextlib import AbstractContextManager, contextmanager from functools import cached_property from random import Random from sys import float_info from typing import ( TYPE_CHECKING, Any, ClassVar, Literal, Optional, TypeAlias, TypedDict, TypeVar, ) from sortedcontainers import SortedSet from hypothesis.errors import HypothesisWarning from hypothesis.internal.cache import LRUCache from hypothesis.internal.compat import WINDOWS, int_from_bytes from hypothesis.internal.conjecture.choice import ( ChoiceConstraintsT, ChoiceT, ChoiceTypeT, FloatConstraints, choice_constraints_key, choice_permitted, ) from hypothesis.internal.conjecture.floats import lex_to_float from hypothesis.internal.conjecture.junkdrawer import bits_to_bytes from hypothesis.internal.conjecture.utils import ( INT_SIZES, INT_SIZES_SAMPLER, Sampler, many, ) from hypothesis.internal.constants_ast import ( Constants, constants_from_module, is_local_module_file, ) from hypothesis.internal.floats import ( SIGNALING_NAN, float_to_int, make_float_clamper, next_down, next_up, ) from hypothesis.internal.intervalsets import IntervalSet from hypothesis.internal.observability import InfoObservationType, TestCaseObservation if TYPE_CHECKING: from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.constants_ast import ConstantT T = TypeVar("T") LifetimeT: TypeAlias = Literal["test_case", "test_function"] COLLECTION_DEFAULT_MAX_SIZE = 10**10 # "arbitrarily large" #: Registered Hypothesis backends. This is a dictionary where keys are the name #: to be used in |settings.backend|. The value of a key can be either: #: #: * A string corresponding to an importable absolute path of a #: |PrimitiveProvider| subclass #: * A |PrimitiveProvider| subclass (the class itself, not an instance of the #: class) #: #: Hypothesis will instantiate the corresponding |PrimitiveProvider| subclass #: when the backend is requested by a test's |settings.backend| value. #: #: For example, the default Hypothesis backend is registered as: #: #: .. code-block:: python #: #: from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS #: #: AVAILABLE_PROVIDERS["hypothesis"] = "hypothesis.internal.conjecture.providers.HypothesisProvider" #: # or #: AVAILABLE_PROVIDERS["hypothesis"] = HypothesisProvider #: #: And can be used with: #: #: .. code-block:: python #: #: from hypothesis import given, settings, strategies as st #: #: @given(st.integers()) #: @settings(backend="hypothesis") #: def f(n): #: pass #: #: Though, as ``backend="hypothesis"`` is the default setting, the above would #: typically not have any effect. #: #: For third-party backend authors, we strongly encourage ensuring that #: ``import hypothesis`` does not automatically import the expensive parts of #: your package, by: #: #: - setting a string path here, instead of a provider class #: - ensuring the registered hypothesis plugin path references a path which just #: sets AVAILABLE_PROVIDERS and does not import your package AVAILABLE_PROVIDERS: dict[str, str | type["PrimitiveProvider"]] = { "hypothesis": "hypothesis.internal.conjecture.providers.HypothesisProvider", "hypothesis-urandom": "hypothesis.internal.conjecture.providers.URandomProvider", } # cache the choice_permitted constants for a particular set of constraints. CacheKeyT: TypeAlias = tuple[ChoiceTypeT, tuple[Any, ...]] CacheValueT: TypeAlias = tuple[tuple["ConstantT", ...], tuple["ConstantT", ...]] CONSTANTS_CACHE: LRUCache[CacheKeyT, CacheValueT] = LRUCache(1024) _constants_integers = ( # powers of 2 [2**n for n in range(16, 66)] # powers of 10 + [10**n for n in range(5, 20)] # factorials + [math.factorial(n) for n in range(9, 21)] # a few primorial numbers https://en.wikipedia.org/wiki/Primorial + [ 510510, 6469693230, 304250263527210, 32589158477190044730, ] ) _constants_integers.extend( [n - 1 for n in _constants_integers] + [n + 1 for n in _constants_integers] ) _constants_integers.extend([-x for x in _constants_integers]) # arbitrary cutoffs to keep our list bounded assert all(50_000 <= abs(n) <= 2**66 for n in _constants_integers) _constant_floats = ( [ 0.5, 1.1, 1.5, 1.9, 1.0 / 3, 10e6, 10e-6, 1.175494351e-38, next_up(0.0), float_info.min, float_info.max, 3.402823466e38, 9007199254740992.0, 1 - 10e-6, 2 + 10e-6, 1.192092896e-07, 2.2204460492503131e-016, ] + [2.0**-n for n in (24, 14, 149, 126)] # minimum (sub)normals for float16,32 + [float_info.min / n for n in (2, 10, 1000, 100_000)] # subnormal in float64 ) _constant_floats.extend([-x for x in _constant_floats]) assert all(isinstance(f, float) for f in _constant_floats) _constant_strings = { # strings which can be interpreted as code / logic "undefined", "null", "NULL", "nil", "NIL", "true", "false", "True", "False", "TRUE", "FALSE", "None", "none", "if", "then", "else", "__dict__", "__proto__", # javascript # strings which can be interpreted as a number "0", "1e100", "0..0", "0/0", "1/0", "+0.0", "Infinity", "-Infinity", "Inf", "INF", "NaN", "9" * 30, # common ascii characters ",./;'[]\\-=<>?:\"{}|_+!@#$%^&*()`~", # common unicode characters "Ω≈ç√∫˜µ≤≥÷åß∂ƒ©˙∆˚¬…æœ∑´®†¥¨ˆøπ“‘¡™£¢∞§¶•ªº–≠¸˛Ç◊ı˜Â¯˘¿ÅÍÎÏ˝ÓÔÒÚÆ☃Œ„´‰ˇÁ¨ˆØ∏”’`⁄€‹›fifl‡°·‚—±", # characters which increase in length when lowercased "Ⱥ", "Ⱦ", # ligatures "æœÆŒffʤʨß", # emoticons "(╯°□°)╯︵ ┻━┻)", # emojis "😍", "🇺🇸", # emoji modifiers "🏻", # U+1F3FB Light Skin Tone, "👍🏻", # 👍 followed by U+1F3FB # RTL text "الكل في المجمو عة", # Ogham text, which contains the only character in the Space Separators # unicode category (Zs) that isn't visually blank:  . # noqa: RUF003 "᚛ᚄᚓᚐᚋᚒᚄ ᚑᚄᚂᚑᚏᚅ᚜", # readable variations on text (bolt/italic/script) "𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠", "𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌", "𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈", "𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰", "𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘", # upsidown text "ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥", # reserved strings in windows "NUL", "COM1", "LPT1", # scunthorpe problem "Scunthorpe", # zalgo text "Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣", # # examples from https://faultlore.com/blah/text-hates-you/ "मनीष منش", "पन्ह पन्ह त्र र्च कृकृ ड्ड न्हृे إلا بسم الله", "lorem لا بسم الله ipsum 你好1234你好", } # we don't actually care what order the constants are sorted in, just that the # ordering is deterministic. GLOBAL_CONSTANTS = Constants( integers=SortedSet(_constants_integers), floats=SortedSet(_constant_floats, key=float_to_int), bytes=SortedSet(), strings=SortedSet(_constant_strings), ) _local_constants = Constants( integers=SortedSet(), floats=SortedSet(key=float_to_int), bytes=SortedSet(), strings=SortedSet(), ) # modules that we've already seen and processed for local constants. These are # are all modules, not necessarily local ones. This lets us quickly see which # modules are new without an expensive path.resolve() or is_local_module_file # cache lookup. # We track by module object when hashable, falling back to the module name # (str key in sys.modules) for unhashable entries like SimpleNamespace. _seen_modules: set = set() _sys_modules_len: int | None = None def _get_local_constants() -> Constants: global _sys_modules_len, _local_constants if sys.platform == "emscripten": # pragma: no cover # pyodide builds bundle the stdlib in a nonstandard location, like # `/lib/python312.zip/heapq.py`. To avoid identifying the entirety of # the stdlib as local code and slowing down on emscripten, instead return # that nothing is local. # # pyodide may provide some way to distinguish stdlib/third-party/local # code. I haven't looked into it. If they do, we should correctly implement # ModuleLocation for pyodide instead of this. return _local_constants count_constants = len(_local_constants) # We call this function once per HypothesisProvider instance, i.e. once per # input, so it needs to be performant. The logic here is more complicated # than necessary because of this. # # First, we check whether there are any new modules with a very cheap length # check. This check can be fooled if a module is added while another module is # removed, but the more correct check against tuple(sys.modules.keys()) is # substantially more expensive. Such a new module would eventually be discovered # if / when the length changes again in the future. # # If the length has changed, we find just modules we haven't seen before. Of # those, we find the ones which correspond to local modules, and extract their # constants. # careful: store sys.modules length when we first check to avoid race conditions # with other threads loading a module before we set _sys_modules_len. if (sys_modules_len := len(sys.modules)) != _sys_modules_len: new_modules = [] for name, module in list(sys.modules.items()): try: seen = module in _seen_modules except TypeError: # unhashable module (e.g. SimpleNamespace); fall back to name seen = name in _seen_modules if not seen: new_modules.append((name, module)) # Repeated SortedSet unions are expensive. Do the initial unions on a # set(), then do a one-time union with _local_constants after. new_constants = Constants() for name, module in new_modules: if ( module_file := getattr(module, "__file__", None) ) is not None and is_local_module_file(module_file): new_constants |= constants_from_module(module) try: _seen_modules.add(module) except TypeError: _seen_modules.add(name) _local_constants |= new_constants _sys_modules_len = sys_modules_len # if we add any new constant, invalidate the constant cache for permitted values. # A more efficient approach would be invalidating just the keys with this # choice_type. if len(_local_constants) > count_constants: CONSTANTS_CACHE.cache.clear() return _local_constants @contextmanager def with_register_backend(name, provider_cls): try: AVAILABLE_PROVIDERS[name] = provider_cls yield finally: del AVAILABLE_PROVIDERS[name] class _BackendInfoMsg(TypedDict): type: InfoObservationType title: str content: str | dict[str, Any] # TODO_DOCS: link to choice sequence explanation page class PrimitiveProvider(abc.ABC): """ |PrimitiveProvider| is the implementation interface of a :ref:`Hypothesis backend `. A |PrimitiveProvider| is required to implement the following five ``draw_*`` methods: * |PrimitiveProvider.draw_integer| * |PrimitiveProvider.draw_boolean| * |PrimitiveProvider.draw_float| * |PrimitiveProvider.draw_string| * |PrimitiveProvider.draw_bytes| Each strategy in Hypothesis generates values by drawing a series of choices from these five methods. By overriding them, a |PrimitiveProvider| can control the distribution of inputs generated by Hypothesis. For example, :pypi:`hypothesis-crosshair` implements a |PrimitiveProvider| which uses an SMT solver to generate inputs that uncover new branches. Once you implement a |PrimitiveProvider|, you can make it available for use through |AVAILABLE_PROVIDERS|. """ #: The lifetime of a |PrimitiveProvider| instance. Either ``test_function`` #: or ``test_case``. #: #: If ``test_function`` (the default), a single provider instance will be #: instantiated and used for the entirety of each test function (i.e., roughly #: one provider per |@given| annotation). This can be useful for tracking state #: over the entirety of a test function. #: #: If ``test_case``, a new provider instance will be instantiated and used for #: each input Hypothesis generates. #: #: The ``conjecturedata`` argument to ``PrimitiveProvider.__init__`` will #: be ``None`` for a lifetime of ``test_function``, and an instance of #: ``ConjectureData`` for a lifetime of ``test_case``. #: #: Third-party providers likely want to set a lifetime of ``test_function``. lifetime: ClassVar[LifetimeT] = "test_function" #: Solver-based backends such as ``hypothesis-crosshair`` use symbolic values #: which record operations performed on them in order to discover new paths. #: If ``avoid_realization`` is set to ``True``, hypothesis will avoid interacting #: with symbolic choices returned by the provider in any way that would force #: the solver to narrow the range of possible values for that symbolic. #: #: Setting this to ``True`` disables some hypothesis features and optimizations. #: Only set this to ``True`` if it is necessary for your backend. avoid_realization: ClassVar[bool] = False #: If ``True``, |PrimitiveProvider.on_observation| will be added as a #: callback via |add_observability_callback|, enabling observability during # the lifetime of this provider. If ``False``, |PrimitiveProvider.on_observation| #: will never be called by Hypothesis. #: #: The opt-in behavior of observability is because enabling observability #: might increase runtime or memory usage. add_observability_callback: ClassVar[bool] = False def __init__(self, conjecturedata: Optional["ConjectureData"], /) -> None: self._cd = conjecturedata @abc.abstractmethod def draw_boolean( self, p: float = 0.5, ) -> bool: """ Draw a boolean choice. Parameters ---------- p: float The probability of returning ``True``. Between 0 and 1 inclusive. Except for ``0`` and ``1``, the value of ``p`` is a hint provided by Hypothesis, and may be ignored by the backend. If ``0``, the provider must return ``False``. If ``1``, the provider must return ``True``. """ raise NotImplementedError @abc.abstractmethod def draw_integer( self, min_value: int | None = None, max_value: int | None = None, *, weights: dict[int, float] | None = None, shrink_towards: int = 0, ) -> int: """ Draw an integer choice. Parameters ---------- min_value : int | None (Inclusive) lower bound on the integer value. If ``None``, there is no lower bound. max_value : int | None (Inclusive) upper bound on the integer value. If ``None``, there is no upper bound. weights: dict[int, float] | None Maps keys in the range [``min_value``, ``max_value``] to the probability of returning that key. shrink_towards: int The integer to shrink towards. This is not used during generation and can be ignored by backends. """ raise NotImplementedError @abc.abstractmethod def draw_float( self, *, min_value: float = -math.inf, max_value: float = math.inf, allow_nan: bool = True, smallest_nonzero_magnitude: float, ) -> float: """ Draw a float choice. Parameters ---------- min_value : float (Inclusive) lower bound on the float value. max_value : float (Inclusive) upper bound on the float value. allow_nan : bool If ``False``, it is invalid to return ``math.nan``. smallest_nonzero_magnitude : float The smallest allowed nonzero magnitude. ``draw_float`` should not return a float ``f`` if ``abs(f) < smallest_nonzero_magnitude``. """ raise NotImplementedError @abc.abstractmethod def draw_string( self, intervals: IntervalSet, *, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, ) -> str: """ Draw a string choice. Parameters ---------- intervals : IntervalSet The set of codepoints to sample from. min_size : int (Inclusive) lower bound on the string length. max_size : int (Inclusive) upper bound on the string length. """ raise NotImplementedError @abc.abstractmethod def draw_bytes( self, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, ) -> bytes: """ Draw a bytes choice. Parameters ---------- min_size : int (Inclusive) lower bound on the bytes length. max_size : int (Inclusive) upper bound on the bytes length. """ raise NotImplementedError def per_test_case_context_manager(self) -> AbstractContextManager: """ Returns a context manager which will be entered each time Hypothesis starts generating and executing one test case, and exited when that test case finishes generating and executing, including if any exception is thrown. In the lifecycle of a Hypothesis test, this is called before generating strategy values for each test case. This is just before any :ref:`custom executor ` is called. Even if not returning a custom context manager, |PrimitiveProvider| subclasses are welcome to override this method to know when Hypothesis starts and ends the execution of a single test case. """ return contextlib.nullcontext() def realize(self, value: T, *, for_failure: bool = False) -> T: """ Called whenever hypothesis requires a concrete (non-symbolic) value from a potentially symbolic value. Hypothesis will not check that ``value`` is symbolic before calling ``realize``, so you should handle the case where ``value`` is non-symbolic. The returned value should be non-symbolic. If you cannot provide a value, raise |BackendCannotProceed| with a value of ``"discard_test_case"``. If ``for_failure`` is ``True``, the value is associated with a failing example. In this case, the backend should spend substantially more effort when attempting to realize the value, since it is important to avoid discarding failing examples. Backends may still raise |BackendCannotProceed| when ``for_failure`` is ``True``, if realization is truly impossible or if realization takes significantly longer than expected (say, 5 minutes). """ return value def replay_choices(self, choices: tuple[ChoiceT, ...]) -> None: """ Called when Hypothesis has discovered a choice sequence which the provider may wish to enqueue to replay under its own instrumentation when we next ask to generate a test case, rather than generating one from scratch. This is used to e.g. warm-start :pypi:`hypothesis-crosshair` with a corpus of high-code-coverage inputs discovered by `HypoFuzz `_. """ return None def observe_test_case(self) -> dict[str, Any]: """Called at the end of the test case when :ref:`observability ` is enabled. The return value should be a non-symbolic json-encodable dictionary, and will be included in observations as ``observation["metadata"]["backend"]``. """ return {} def observe_information_messages( self, *, lifetime: LifetimeT ) -> Iterable[_BackendInfoMsg]: """Called at the end of each test case and again at end of the test function. Return an iterable of ``{type: info/alert/error, title: str, content: str | dict}`` dictionaries to be delivered as individual information messages. Hypothesis adds the ``run_start`` timestamp and ``property`` name for you. """ assert lifetime in ("test_case", "test_function") yield from [] def on_observation(self, observation: TestCaseObservation) -> None: # noqa: B027 """ Called at the end of each test case which uses this provider, with the same ``observation["type"] == "test_case"`` observation that is passed to other callbacks added via |add_observability_callback|. This method is not called with ``observation["type"] in {"info", "alert", "error"}`` observations. .. important:: For |PrimitiveProvider.on_observation| to be called by Hypothesis, |PrimitiveProvider.add_observability_callback| must be set to ``True``. |PrimitiveProvider.on_observation| is explicitly opt-in, as enabling observability might increase runtime or memory usage. Calls to this method are guaranteed to alternate with calls to |PrimitiveProvider.per_test_case_context_manager|. For example: .. code-block:: python # test function starts per_test_case_context_manager() on_observation() per_test_case_context_manager() on_observation() ... # test function ends Note that |PrimitiveProvider.on_observation| will not be called for test cases which did not use this provider during generation, for example during |Phase.reuse| or |Phase.shrink|, or because Hypothesis switched to the standard Hypothesis backend after this backend raised too many |BackendCannotProceed| exceptions. """ def span_start(self, label: int, /) -> None: # noqa: B027 # non-abstract noop """Marks the beginning of a semantically meaningful span of choices. Spans are a depth-first tree structure. A span is opened by a call to |PrimitiveProvider.span_start|, and a call to |PrimitiveProvider.span_end| closes the most recently opened span. So the following sequence of calls: .. code-block:: python span_start(label=1) n1 = draw_integer() span_start(label=2) b1 = draw_boolean() n2 = draw_integer() span_end() f1 = draw_float() span_end() produces the following two spans of choices: .. code-block:: 1: [n1, b1, n2, f1] 2: [b1, n2] Hypothesis uses spans to denote "semantically meaningful" sequences of choices. For instance, Hypothesis opens a span for the sequence of choices made while drawing from each strategy. Not every span corresponds to a strategy; the generation of e.g. each element in |st.lists| is also marked with a span, among others. ``label`` is an opaque integer, which has no defined semantics. The only guarantee made by Hypothesis is that all spans with the same "meaning" will share the same ``label``. So all spans from the same strategy will share the same label, as will e.g. the spans for |st.lists| elements. Providers can track calls to |PrimitiveProvider.span_start| and |PrimitiveProvider.span_end| to learn something about the semantics of the test's choice sequence. For instance, a provider could track the depth of the span tree, or the number of unique labels, which says something about the complexity of the choices being generated. Or a provider could track the span tree across test cases in order to determine what strategies are being used in what contexts. It is possible for Hypothesis to start and immediately stop a span, without calling a ``draw_*`` method in between. These spans contain zero choices. Hypothesis will always balance the number of calls to |PrimitiveProvider.span_start| and |PrimitiveProvider.span_end|. A call to |PrimitiveProvider.span_start| will always be followed by a call to |PrimitiveProvider.span_end| before the end of the test case. |PrimitiveProvider.span_start| is called from ``ConjectureData.start_span()`` internally. """ def span_end(self, discard: bool, /) -> None: # noqa: B027 """Marks the end of a semantically meaningful span of choices. ``discard`` is ``True`` when the draw was filtered out or otherwise marked as unlikely to contribute to the input data as seen by the user's test. Note however that side effects can make this determination unsound. |PrimitiveProvider.span_end| is called from ``ConjectureData.stop_span()`` internally. """ class HypothesisProvider(PrimitiveProvider): lifetime = "test_case" def __init__(self, conjecturedata: Optional["ConjectureData"], /): super().__init__(conjecturedata) self._random = None if self._cd is None else self._cd._random @cached_property def _local_constants(self): # defer computation of local constants until/if we need it return _get_local_constants() def _maybe_draw_constant( self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, p: float = 0.05, ) -> Optional["ConstantT"]: assert self._random is not None assert choice_type != "boolean" # check whether we even want a constant before spending time computing # and caching the allowed constants. if self._random.random() > p: return None # note: this property access results in computation being done assert self._local_constants is not None key = (choice_type, choice_constraints_key(choice_type, constraints)) if key not in CONSTANTS_CACHE: CONSTANTS_CACHE[key] = ( tuple( choice for choice in GLOBAL_CONSTANTS.set_for_type(choice_type) if choice_permitted(choice, constraints) ), tuple( choice for choice in self._local_constants.set_for_type(choice_type) if choice_permitted(choice, constraints) ), ) # split constants into two pools, so we still have a good chance to draw # global constants even if there are many local constants. global_constants, local_constants = CONSTANTS_CACHE[key] constants_lists = ([global_constants] if global_constants else []) + ( [local_constants] if local_constants else [] ) if not constants_lists: return None # At this point, we've decided to use a constant. Now we select which pool # to draw that constant from. # # Note that this approach has a different probability distribution than # attempting a random.random for both global_constants and local_constants. constants = self._random.choice(constants_lists) return self._random.choice(constants) def draw_boolean( self, p: float = 0.5, ) -> bool: assert self._random is not None if p <= 0: return False if p >= 1: return True return self._random.random() < p def draw_integer( self, min_value: int | None = None, max_value: int | None = None, *, weights: dict[int, float] | None = None, shrink_towards: int = 0, ) -> int: assert self._cd is not None if ( constant := self._maybe_draw_constant( "integer", { "min_value": min_value, "max_value": max_value, "weights": weights, "shrink_towards": shrink_towards, }, ) ) is not None: assert isinstance(constant, int) return constant center = 0 if min_value is not None: center = max(min_value, center) if max_value is not None: center = min(max_value, center) if weights is not None: assert min_value is not None assert max_value is not None # format of weights is a mapping of ints to p, where sum(p) < 1. # The remaining probability mass is uniformly distributed over # *all* ints (not just the unmapped ones; this is somewhat undesirable, # but simplifies things). # # We assert that sum(p) is strictly less than 1 because it simplifies # handling forced values when we can force into the unmapped probability # mass. We should eventually remove this restriction. sampler = Sampler( [1 - sum(weights.values()), *weights.values()], observe=False ) # if we're forcing, it's easiest to force into the unmapped probability # mass and then force the drawn value after. idx = sampler.sample(self._cd) if idx == 0: return self._draw_bounded_integer(min_value, max_value) # implicit reliance on dicts being sorted for determinism return list(weights)[idx - 1] if min_value is None and max_value is None: return self._draw_unbounded_integer() if min_value is None: assert max_value is not None probe = max_value + 1 while max_value < probe: probe = center + self._draw_unbounded_integer() return probe if max_value is None: assert min_value is not None probe = min_value - 1 while probe < min_value: probe = center + self._draw_unbounded_integer() return probe return self._draw_bounded_integer(min_value, max_value) def draw_float( self, *, min_value: float = -math.inf, max_value: float = math.inf, allow_nan: bool = True, smallest_nonzero_magnitude: float, ) -> float: assert self._random is not None constraints: FloatConstraints = { "min_value": min_value, "max_value": max_value, "allow_nan": allow_nan, "smallest_nonzero_magnitude": smallest_nonzero_magnitude, } if ( constant := self._maybe_draw_constant("float", constraints, p=0.15) ) is not None: assert isinstance(constant, float) return constant # on top of the probability to draw a constant float, we independently # upweight 0.0/-0.0, math.inf, -math.inf, nans, and boundary values. weird_floats = [ f for f in [ 0.0, -0.0, math.inf, -math.inf, math.nan, -math.nan, SIGNALING_NAN, -SIGNALING_NAN, min_value, next_up(min_value), min_value + 1, max_value - 1, next_down(max_value), max_value, ] if choice_permitted(f, constraints) ] if weird_floats and self._random.random() < 0.05: return self._random.choice(weird_floats) clamper = make_float_clamper( min_value, max_value, smallest_nonzero_magnitude=smallest_nonzero_magnitude, allow_nan=allow_nan, ) result = self._draw_float() if allow_nan and math.isnan(result): clamped = result # pragma: no cover else: clamped = clamper(result) if float_to_int(clamped) != float_to_int(result) and not ( math.isnan(result) and allow_nan ): result = clamped return result def draw_string( self, intervals: IntervalSet, *, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, ) -> str: assert self._cd is not None assert self._random is not None if len(intervals) == 0: return "" if ( constant := self._maybe_draw_constant( "string", {"intervals": intervals, "min_size": min_size, "max_size": max_size}, ) ) is not None: assert isinstance(constant, str) return constant average_size = min( max(min_size * 2, min_size + 5), 0.5 * (min_size + max_size), ) chars = [] elements = many( self._cd, min_size=min_size, max_size=max_size, average_size=average_size, observe=False, ) while elements.more(): if len(intervals) > 256: if self.draw_boolean(0.2): i = self._random.randint(256, len(intervals) - 1) else: i = self._random.randint(0, 255) else: i = self._random.randint(0, len(intervals) - 1) chars.append(intervals.char_in_shrink_order(i)) return "".join(chars) def draw_bytes( self, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, ) -> bytes: assert self._cd is not None assert self._random is not None if ( constant := self._maybe_draw_constant( "bytes", {"min_size": min_size, "max_size": max_size} ) ) is not None: assert isinstance(constant, bytes) return constant buf = bytearray() average_size = min( max(min_size * 2, min_size + 5), 0.5 * (min_size + max_size), ) elements = many( self._cd, min_size=min_size, max_size=max_size, average_size=average_size, observe=False, ) while elements.more(): buf += self._random.randbytes(1) return bytes(buf) def _draw_float(self) -> float: assert self._random is not None f = lex_to_float(self._random.getrandbits(64)) sign = 1 if self._random.getrandbits(1) else -1 return sign * f def _draw_unbounded_integer(self) -> int: assert self._cd is not None assert self._random is not None size = INT_SIZES[INT_SIZES_SAMPLER.sample(self._cd)] r = self._random.getrandbits(size) sign = r & 1 r >>= 1 if sign: r = -r return r def _draw_bounded_integer( self, lower: int, upper: int, *, vary_size: bool = True, ) -> int: assert lower <= upper assert self._cd is not None assert self._random is not None if lower == upper: return lower bits = (upper - lower).bit_length() if bits > 24 and vary_size and self._random.random() < 7 / 8: # For large ranges, we combine the uniform random distribution # with a weighting scheme with moderate chance. Cutoff at 2 ** 24 so that our # choice of unicode characters is uniform but the 32bit distribution is not. idx = INT_SIZES_SAMPLER.sample(self._cd) cap_bits = min(bits, INT_SIZES[idx]) upper = min(upper, lower + 2**cap_bits - 1) return self._random.randint(lower, upper) return self._random.randint(lower, upper) # Masks for masking off the first byte of an n-bit buffer. # The appropriate mask is stored at position n % 8. BYTE_MASKS = [(1 << n) - 1 for n in range(8)] BYTE_MASKS[0] = 255 class BytestringProvider(PrimitiveProvider): lifetime = "test_case" def __init__( self, conjecturedata: Optional["ConjectureData"], /, *, bytestring: bytes ): super().__init__(conjecturedata) self.bytestring = bytestring self.index = 0 self.drawn = bytearray() def _draw_bits(self, n): if n == 0: # pragma: no cover return 0 n_bytes = bits_to_bytes(n) if self.index + n_bytes > len(self.bytestring): self._cd.mark_overrun() buf = bytearray(self.bytestring[self.index : self.index + n_bytes]) self.index += n_bytes buf[0] &= BYTE_MASKS[n % 8] buf = bytes(buf) self.drawn += buf return int_from_bytes(buf) def draw_boolean( self, p: float = 0.5, ) -> bool: if p <= 0: return False if p >= 1: return True # always use one byte for booleans to maintain constant draw size. # If a probability requires more than 8 bits to represent precisely, # the result will be slightly biased, but not badly. bits = 8 size = 2**bits # always leave at least one value that can be true, even for very small # p. falsey = max(1, math.floor(size * (1 - p))) n = self._draw_bits(bits) return n >= falsey def draw_integer( self, min_value: int | None = None, max_value: int | None = None, *, weights: dict[int, float] | None = None, shrink_towards: int = 0, ) -> int: assert self._cd is not None # we explicitly ignore integer weights for now, as they are likely net # negative on fuzzer performance. if min_value is None and max_value is None: min_value = -(2**127) max_value = 2**127 - 1 elif min_value is None: assert max_value is not None min_value = max_value - 2**64 elif max_value is None: assert min_value is not None max_value = min_value + 2**64 if min_value == max_value: return min_value bits = (max_value - min_value).bit_length() value = self._draw_bits(bits) while not (min_value <= value <= max_value): value = self._draw_bits(bits) return value def draw_float( self, *, min_value: float = -math.inf, max_value: float = math.inf, allow_nan: bool = True, smallest_nonzero_magnitude: float, ) -> float: n = self._draw_bits(64) sign = -1 if n >> 64 else 1 f = sign * lex_to_float(n & ((1 << 64) - 1)) clamper = make_float_clamper( min_value, max_value, smallest_nonzero_magnitude=smallest_nonzero_magnitude, allow_nan=allow_nan, ) return clamper(f) def _draw_collection(self, min_size, max_size, *, alphabet_size): average_size = min( max(min_size * 2, min_size + 5), 0.5 * (min_size + max_size), ) elements = many( self._cd, min_size=min_size, max_size=max_size, average_size=average_size, observe=False, ) values = [] while elements.more(): values.append(self.draw_integer(0, alphabet_size - 1)) return values def draw_string( self, intervals: IntervalSet, *, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, ) -> str: values = self._draw_collection(min_size, max_size, alphabet_size=len(intervals)) return "".join(chr(intervals[v]) for v in values) def draw_bytes( self, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, ) -> bytes: values = self._draw_collection(min_size, max_size, alphabet_size=2**8) return bytes(values) class URandom(Random): # we reimplement a Random instance instead of using SystemRandom, because # os.urandom is not guaranteed to read from /dev/urandom. @staticmethod def _urandom(size: int) -> bytes: with open("/dev/urandom", "rb") as f: return f.read(size) def getrandbits(self, k: int) -> int: assert k >= 0 size = bits_to_bytes(k) n = int_from_bytes(self._urandom(size)) # trim excess bits return n >> (size * 8 - k) def random(self) -> float: # adapted from random.SystemRandom.random return (int_from_bytes(self._urandom(7)) >> 3) * (2**-53) class URandomProvider(HypothesisProvider): # A provider which reads directly from /dev/urandom as its source of randomness. # This provider exists to provide better Hypothesis integration with Antithesis # (https://antithesis.com/), which interprets calls to /dev/urandom as the # randomness to mutate. This effectively gives Antithesis control over # the choices made by the URandomProvider. # # If you are not using Antithesis, you probably don't want to use this # provider. def __init__(self, conjecturedata: Optional["ConjectureData"], /): super().__init__(conjecturedata) if WINDOWS: # pragma: no cover warnings.warn( "/dev/urandom is not available on windows. Falling back to " 'standard PRNG generation (equivalent to backend="hypothesis").', HypothesisWarning, stacklevel=1, ) # don't overwrite the HypothesisProvider self._random attribute in # this case else: self._random = URandom() ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from collections import defaultdict from collections.abc import Callable, Sequence from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, Literal, TypeAlias, cast, ) from hypothesis.internal.conjecture.choice import ( ChoiceNode, ChoiceT, choice_equal, choice_from_index, choice_key, choice_permitted, choice_to_index, ) from hypothesis.internal.conjecture.data import ( ConjectureData, ConjectureResult, Spans, Status, _Overrun, draw_choice, ) from hypothesis.internal.conjecture.junkdrawer import ( endswith, find_integer, replace_all, startswith, ) from hypothesis.internal.conjecture.shrinking import ( Bytes, Float, Integer, Ordering, String, ) from hypothesis.internal.conjecture.shrinking.choicetree import ( ChoiceTree, prefix_selection_order, random_selection_order, ) from hypothesis.internal.floats import MAX_PRECISE_INTEGER if TYPE_CHECKING: from random import Random from hypothesis.internal.conjecture.engine import ConjectureRunner ShrinkPredicateT: TypeAlias = Callable[[ConjectureResult | _Overrun], bool] def sort_key(nodes: Sequence[ChoiceNode]) -> tuple[int, tuple[int, ...]]: """Returns a sort key such that "simpler" choice sequences are smaller than "more complicated" ones. We define sort_key so that x is simpler than y if x is shorter than y or if they have the same length and map(choice_to_index, x) < map(choice_to_index, y). The reason for using this ordering is: 1. If x is shorter than y then that means we had to make fewer decisions in constructing the test case when we ran x than we did when we ran y. 2. If x is the same length as y then replacing a choice with a lower index choice corresponds to replacing it with a simpler/smaller choice. 3. Because choices drawn early in generation potentially get used in more places they potentially have a more significant impact on the final result, so it makes sense to prioritise reducing earlier choices over later ones. """ return ( len(nodes), tuple(choice_to_index(node.value, node.constraints) for node in nodes), ) @dataclass(slots=True, frozen=False) class ShrinkPass: function: Any name: str | None = None last_prefix: Any = () # some execution statistics calls: int = 0 misaligned: int = 0 shrinks: int = 0 deletions: int = 0 def __post_init__(self): if self.name is None: self.name = self.function.__name__ def __hash__(self): return hash(self.name) class StopShrinking(Exception): pass class Shrinker: """A shrinker is a child object of a ConjectureRunner which is designed to manage the associated state of a particular shrink problem. That is, we have some initial ConjectureData object and some property of interest that it satisfies, and we want to find a ConjectureData object with a shortlex (see sort_key above) smaller choice sequence that exhibits the same property. Currently the only property of interest we use is that the status is INTERESTING and the interesting_origin takes on some fixed value, but we may potentially be interested in other use cases later. However we assume that data with a status < VALID never satisfies the predicate. The shrinker keeps track of a value shrink_target which represents the current best known ConjectureData object satisfying the predicate. It refines this value by repeatedly running *shrink passes*, which are methods that perform a series of transformations to the current shrink_target and evaluate the underlying test function to find new ConjectureData objects. If any of these satisfy the predicate, the shrink_target is updated automatically. Shrinking runs until no shrink pass can improve the shrink_target, at which point it stops. It may also be terminated if the underlying engine throws RunIsComplete, but that is handled by the calling code rather than the Shrinker. ======================= Designing Shrink Passes ======================= Generally a shrink pass is just any function that calls cached_test_function and/or consider_new_nodes a number of times, but there are a couple of useful things to bear in mind. A shrink pass *makes progress* if running it changes self.shrink_target (i.e. it tries a shortlex smaller ConjectureData object satisfying the predicate). The desired end state of shrinking is to find a value such that no shrink pass can make progress, i.e. that we are at a local minimum for each shrink pass. In aid of this goal, the main invariant that a shrink pass much satisfy is that whether it makes progress must be deterministic. It is fine (encouraged even) for the specific progress it makes to be non-deterministic, but if you run a shrink pass, it makes no progress, and then you immediately run it again, it should never succeed on the second time. This allows us to stop as soon as we have run each shrink pass and seen no progress on any of them. This means that e.g. it's fine to try each of N deletions or replacements in a random order, but it's not OK to try N random deletions (unless you have already shrunk at least once, though we don't currently take advantage of this loophole). Shrink passes need to be written so as to be robust against change in the underlying shrink target. It is generally safe to assume that the shrink target does not change prior to the point of first modification - e.g. if you change no bytes at index ``i``, all spans whose start is ``<= i`` still exist, as do all blocks, and the data object is still of length ``>= i + 1``. This can only be violated by bad user code which relies on an external source of non-determinism. When the underlying shrink_target changes, shrink passes should not run substantially more test_function calls on success than they do on failure. Say, no more than a constant factor more. In particular shrink passes should not iterate to a fixed point. This means that shrink passes are often written with loops that are carefully designed to do the right thing in the case that no shrinks occurred and try to adapt to any changes to do a reasonable job. e.g. say we wanted to write a shrink pass that tried deleting each individual choice (this isn't an especially good pass, but it leads to a simple illustrative example), we might do it by iterating over the choice sequence like so: .. code-block:: python i = 0 while i < len(self.shrink_target.nodes): if not self.consider_new_nodes( self.shrink_target.nodes[:i] + self.shrink_target.nodes[i + 1 :] ): i += 1 The reason for writing the loop this way is that i is always a valid index into the current choice sequence, even if the current sequence changes as a result of our actions. When the choice sequence changes, we leave the index where it is rather than restarting from the beginning, and carry on. This means that the number of steps we run in this case is always bounded above by the number of steps we would run if nothing works. Another thing to bear in mind about shrink pass design is that they should prioritise *progress*. If you have N operations that you need to run, you should try to order them in such a way as to avoid stalling, where you have long periods of test function invocations where no shrinks happen. This is bad because whenever we shrink we reduce the amount of work the shrinker has to do in future, and often speed up the test function, so we ideally wanted those shrinks to happen much earlier in the process. Sometimes stalls are inevitable of course - e.g. if the pass makes no progress, then the entire thing is just one long stall, but it's helpful to design it so that stalls are less likely in typical behaviour. The two easiest ways to do this are: * Just run the N steps in random order. As long as a reasonably large proportion of the operations succeed, this guarantees the expected stall length is quite short. The book keeping for making sure this does the right thing when it succeeds can be quite annoying. * When you have any sort of nested loop, loop in such a way that both loop variables change each time. This prevents stalls which occur when one particular value for the outer loop is impossible to make progress on, rendering the entire inner loop into a stall. However, although progress is good, too much progress can be a bad sign! If you're *only* seeing successful reductions, that's probably a sign that you are making changes that are too timid. Two useful things to offset this: * It's worth writing shrink passes which are *adaptive*, in the sense that when operations seem to be working really well we try to bundle multiple of them together. This can often be used to turn what would be O(m) successful calls into O(log(m)). * It's often worth trying one or two special minimal values before trying anything more fine grained (e.g. replacing the whole thing with zero). """ def derived_value(fn): """It's useful during shrinking to have access to derived values of the current shrink target. This decorator allows you to define these as cached properties. They are calculated once, then cached until the shrink target changes, then recalculated the next time they are used.""" def accept(self): try: return self.__derived_values[fn.__name__] except KeyError: return self.__derived_values.setdefault(fn.__name__, fn(self)) accept.__name__ = fn.__name__ return property(accept) def __init__( self, engine: "ConjectureRunner", initial: ConjectureData | ConjectureResult, predicate: ShrinkPredicateT | None, *, allow_transition: ( Callable[[ConjectureData | ConjectureResult, ConjectureData], bool] | None ), explain: bool, in_target_phase: bool = False, ): """Create a shrinker for a particular engine, with a given starting point and predicate. When shrink() is called it will attempt to find an example for which predicate is True and which is strictly smaller than initial. Note that initial is a ConjectureData object, and predicate takes ConjectureData objects. """ assert predicate is not None or allow_transition is not None self.engine = engine self.__predicate = predicate or (lambda data: True) self.__allow_transition = allow_transition or (lambda source, destination: True) self.__derived_values: dict = {} self.initial_size = len(initial.choices) # We keep track of the current best example on the shrink_target # attribute. self.shrink_target = initial self.clear_change_tracking() self.shrinks = 0 # We terminate shrinks that seem to have reached their logical # conclusion: If we've called the underlying test function at # least self.max_stall times since the last time we shrunk, # it's time to stop shrinking. self.max_stall = 200 self.initial_calls = self.engine.call_count self.initial_misaligned = self.engine.misaligned_count self.calls_at_last_shrink = self.initial_calls self.shrink_passes: list[ShrinkPass] = [ ShrinkPass(self.try_trivial_spans), self.node_program("X" * 5), self.node_program("X" * 4), self.node_program("X" * 3), self.node_program("X" * 2), self.node_program("X" * 1), ShrinkPass(self.pass_to_descendant), ShrinkPass(self.reorder_spans), ShrinkPass(self.minimize_duplicated_choices), ShrinkPass(self.minimize_individual_choices), ShrinkPass(self.redistribute_numeric_pairs), ShrinkPass(self.lower_integers_together), ShrinkPass(self.lower_duplicated_characters), ] # Because the shrinker is also used to `pareto_optimise` in the target phase, # we sometimes want to allow extending buffers instead of aborting at the end. self.__extend: Literal["full"] | int = "full" if in_target_phase else 0 self.should_explain = explain @derived_value # type: ignore def cached_calculations(self): return {} def cached(self, *keys): def accept(f): cache_key = (f.__name__, *keys) try: return self.cached_calculations[cache_key] except KeyError: return self.cached_calculations.setdefault(cache_key, f()) return accept @property def calls(self) -> int: """Return the number of calls that have been made to the underlying test function.""" return self.engine.call_count @property def misaligned(self) -> int: return self.engine.misaligned_count def check_calls(self) -> None: if self.calls - self.calls_at_last_shrink >= self.max_stall: raise StopShrinking def cached_test_function( self, nodes: Sequence[ChoiceNode] ) -> tuple[bool, ConjectureResult | _Overrun | None]: nodes = nodes[: len(self.nodes)] if startswith(nodes, self.nodes): return (True, None) if sort_key(self.nodes) < sort_key(nodes): return (False, None) # sometimes our shrinking passes try obviously invalid things. We handle # discarding them in one place here. if any(not choice_permitted(node.value, node.constraints) for node in nodes): return (False, None) result = self.engine.cached_test_function( [n.value for n in nodes], extend=self.__extend ) previous = self.shrink_target self.incorporate_test_data(result) self.check_calls() return (previous is not self.shrink_target, result) def consider_new_nodes(self, nodes: Sequence[ChoiceNode]) -> bool: return self.cached_test_function(nodes)[0] def incorporate_test_data(self, data): """Takes a ConjectureData or Overrun object updates the current shrink_target if this data represents an improvement over it.""" if data.status < Status.VALID or data is self.shrink_target: return if ( self.__predicate(data) and sort_key(data.nodes) < sort_key(self.shrink_target.nodes) and self.__allow_transition(self.shrink_target, data) ): self.update_shrink_target(data) def debug(self, msg: str) -> None: self.engine.debug(msg) @property def random(self) -> "Random": return self.engine.random def shrink(self) -> None: """Run the full set of shrinks and update shrink_target. This method is "mostly idempotent" - calling it twice is unlikely to have any effect, though it has a non-zero probability of doing so. """ try: self.initial_coarse_reduction() self.greedy_shrink() except StopShrinking: # If we stopped shrinking because we're making slow progress (instead of # reaching a local optimum), don't run the explain-phase logic. self.should_explain = False finally: if self.engine.report_debug_info: def s(n): return "s" if n != 1 else "" total_deleted = self.initial_size - len(self.shrink_target.choices) calls = self.engine.call_count - self.initial_calls misaligned = self.engine.misaligned_count - self.initial_misaligned self.debug( "---------------------\n" "Shrink pass profiling\n" "---------------------\n\n" f"Shrinking made a total of {calls} call{s(calls)} of which " f"{self.shrinks} shrank and {misaligned} were misaligned. This " f"deleted {total_deleted} choices out of {self.initial_size}." ) for useful in [True, False]: self.debug("") if useful: self.debug("Useful passes:") else: self.debug("Useless passes:") self.debug("") for pass_ in sorted( self.shrink_passes, key=lambda t: (-t.calls, t.deletions, t.shrinks), ): if pass_.calls == 0: continue if (pass_.shrinks != 0) != useful: continue self.debug( f" * {pass_.name} made {pass_.calls} call{s(pass_.calls)} of which " f"{pass_.shrinks} shrank and {pass_.misaligned} were misaligned, " f"deleting {pass_.deletions} choice{s(pass_.deletions)}." ) self.debug("") self.explain() def explain(self) -> None: if not self.should_explain or not self.shrink_target.arg_slices: return self.max_stall = 2**100 shrink_target = self.shrink_target nodes = self.nodes choices = self.choices chunks: dict[tuple[int, int], list[tuple[ChoiceT, ...]]] = defaultdict(list) # Before we start running experiments, let's check for known inputs which would # make them redundant. The shrinking process means that we've already tried many # variations on the minimal example, so this can save a lot of time. seen_passing_seq = self.engine.passing_choice_sequences( prefix=self.nodes[: min(self.shrink_target.arg_slices)[0]] ) # Now that we've shrunk to a minimal failing example, it's time to try # varying each part that we've noted will go in the final report. Consider # slices in largest-first order for start, end in sorted( self.shrink_target.arg_slices, key=lambda x: (-(x[1] - x[0]), x) ): # Check for any previous examples that match the prefix and suffix, # so we can skip if we found a passing example while shrinking. if any( startswith(seen, nodes[:start]) and endswith(seen, nodes[end:]) for seen in seen_passing_seq ): continue # Skip slices that are subsets of already-explained slices. # If a larger slice can vary freely, so can its sub-slices. # Note: (0, 0) is a special marker for the "together" comment that # applies to the whole test, not a specific slice, so we exclude it. if any( s <= start and end <= e for s, e in self.shrink_target.slice_comments if (s, e) != (0, 0) ): continue # Run our experiments n_same_failures = 0 note = "or any other generated value" # TODO: is 100 same-failures out of 500 attempts a good heuristic? for n_attempt in range(500): # pragma: no branch # no-branch here because we don't coverage-test the abort-at-500 logic. if n_attempt - 10 > n_same_failures * 5: # stop early if we're seeing mostly invalid examples break # pragma: no cover # replace start:end with random values replacement = [] for i in range(start, end): node = nodes[i] if not node.was_forced: value = draw_choice( node.type, node.constraints, random=self.random ) node = node.copy(with_value=value) replacement.append(node.value) attempt = choices[:start] + tuple(replacement) + choices[end:] result = self.engine.cached_test_function(attempt, extend="full") if result.status is Status.OVERRUN: continue # pragma: no cover # flakily covered result = cast(ConjectureResult, result) if not ( len(attempt) == len(result.choices) and endswith(result.nodes, nodes[end:]) ): # Turns out this was a variable-length part, so grab the infix... for span1, span2 in zip( shrink_target.spans, result.spans, strict=False ): assert span1.start == span2.start assert span1.start <= start assert span1.label == span2.label if span1.start == start and span1.end == end: result_end = span2.end break else: raise NotImplementedError("Expected matching prefixes") attempt = ( choices[:start] + result.choices[start:result_end] + choices[end:] ) chunks[(start, end)].append(result.choices[start:result_end]) result = self.engine.cached_test_function(attempt) if result.status is Status.OVERRUN: continue # pragma: no cover # flakily covered result = cast(ConjectureResult, result) else: chunks[(start, end)].append(result.choices[start:end]) if shrink_target is not self.shrink_target: # pragma: no cover # If we've shrunk further without meaning to, bail out. self.shrink_target.slice_comments.clear() return if result.status is Status.VALID: # The test passed, indicating that this param can't vary freely. # However, it's really hard to write a simple and reliable covering # test, because of our `seen_passing_buffers` check above. break # pragma: no cover if self.__predicate(result): # pragma: no branch n_same_failures += 1 if n_same_failures >= 100: self.shrink_target.slice_comments[(start, end)] = note break # Finally, if we've found multiple independently-variable parts, check whether # they can all be varied together. if len(self.shrink_target.slice_comments) <= 1: return n_same_failures_together = 0 # Only include slices that were actually added to slice_comments chunks_by_start_index = sorted( (k, v) for k, v in chunks.items() if k in self.shrink_target.slice_comments ) for _ in range(500): # pragma: no branch # no-branch here because we don't coverage-test the abort-at-500 logic. new_choices: list[ChoiceT] = [] prev_end = 0 for (start, end), ls in chunks_by_start_index: assert prev_end <= start < end, "these chunks must be nonoverlapping" new_choices.extend(choices[prev_end:start]) new_choices.extend(self.random.choice(ls)) prev_end = end result = self.engine.cached_test_function(new_choices) # This *can't* be a shrink because none of the components were. assert shrink_target is self.shrink_target if result.status == Status.VALID: self.shrink_target.slice_comments[(0, 0)] = ( "The test sometimes passed when commented parts were varied together." ) break # Test passed, this param can't vary freely. if self.__predicate(result): # pragma: no branch n_same_failures_together += 1 if n_same_failures_together >= 100: self.shrink_target.slice_comments[(0, 0)] = ( "The test always failed when commented parts were varied together." ) break def greedy_shrink(self) -> None: """Run a full set of greedy shrinks (that is, ones that will only ever move to a better target) and update shrink_target appropriately. This method iterates to a fixed point and so is idempontent - calling it twice will have exactly the same effect as calling it once. """ self.fixate_shrink_passes(self.shrink_passes) def initial_coarse_reduction(self): """Performs some preliminary reductions that should not be repeated as part of the main shrink passes. The main reason why these can't be included as part of shrink passes is that they have much more ability to make the test case "worse". e.g. they might rerandomise part of it, significantly increasing the value of individual nodes, which works in direct opposition to the lexical shrinking and will frequently undo its work. """ self.reduce_each_alternative() @derived_value # type: ignore def spans_starting_at(self): result = [[] for _ in self.shrink_target.nodes] for i, ex in enumerate(self.spans): # We can have zero-length spans that start at the end if ex.start < len(result): result[ex.start].append(i) return tuple(map(tuple, result)) def reduce_each_alternative(self): """This is a pass that is designed to rerandomise use of the one_of strategy or things that look like it, in order to try to move from later strategies to earlier ones in the branch order. It does this by trying to systematically lower each value it finds that looks like it might be the branch decision for one_of, and then attempts to repair any changes in shape that this causes. """ i = 0 while i < len(self.shrink_target.nodes): nodes = self.shrink_target.nodes node = nodes[i] if ( node.type == "integer" and not node.was_forced and node.value <= 10 and node.constraints["min_value"] == 0 ): assert isinstance(node.value, int) # We've found a plausible candidate for a ``one_of`` choice. # We now want to see if the shape of the test case actually depends # on it. If it doesn't, then we don't need to do this (comparatively # costly) pass, and can let much simpler lexicographic reduction # handle it later. # # We test this by trying to set the value to zero and seeing if the # shape changes, as measured by either changing the number of subsequent # nodes, or changing the nodes in such a way as to cause one of the # previous values to no longer be valid in its position. zero_attempt = self.cached_test_function( nodes[:i] + (nodes[i].copy(with_value=0),) + nodes[i + 1 :] )[1] if ( zero_attempt is not self.shrink_target and zero_attempt is not None and zero_attempt.status >= Status.VALID ): changed_shape = len(zero_attempt.nodes) != len(nodes) if not changed_shape: for j in range(i + 1, len(nodes)): zero_node = zero_attempt.nodes[j] orig_node = nodes[j] if ( zero_node.type != orig_node.type or not choice_permitted( orig_node.value, zero_node.constraints ) ): changed_shape = True break if changed_shape: for v in range(node.value): if self.try_lower_node_as_alternative(i, v): break i += 1 def try_lower_node_as_alternative(self, i, v): """Attempt to lower `self.shrink_target.nodes[i]` to `v`, while rerandomising and attempting to repair any subsequent changes to the shape of the test case that this causes.""" nodes = self.shrink_target.nodes if self.consider_new_nodes( nodes[:i] + (nodes[i].copy(with_value=v),) + nodes[i + 1 :] ): return True prefix = nodes[:i] + (nodes[i].copy(with_value=v),) initial = self.shrink_target spans = self.spans_starting_at[i] for _ in range(3): random_attempt = self.engine.cached_test_function( [n.value for n in prefix], extend=len(nodes) ) if random_attempt.status < Status.VALID: continue self.incorporate_test_data(random_attempt) for j in spans: initial_span = initial.spans[j] attempt_span = random_attempt.spans[j] contents = random_attempt.nodes[attempt_span.start : attempt_span.end] self.consider_new_nodes( nodes[:i] + contents + nodes[initial_span.end :] ) if initial is not self.shrink_target: return True return False @derived_value # type: ignore def shrink_pass_choice_trees(self) -> dict[Any, ChoiceTree]: return defaultdict(ChoiceTree) def step(self, shrink_pass: ShrinkPass, *, random_order: bool = False) -> bool: tree = self.shrink_pass_choice_trees[shrink_pass] if tree.exhausted: return False initial_shrinks = self.shrinks initial_calls = self.calls initial_misaligned = self.misaligned size = len(self.shrink_target.choices) assert shrink_pass.name is not None self.engine.explain_next_call_as(shrink_pass.name) if random_order: selection_order = random_selection_order(self.random) else: selection_order = prefix_selection_order(shrink_pass.last_prefix) try: shrink_pass.last_prefix = tree.step( selection_order, lambda chooser: shrink_pass.function(chooser), ) finally: shrink_pass.calls += self.calls - initial_calls shrink_pass.misaligned += self.misaligned - initial_misaligned shrink_pass.shrinks += self.shrinks - initial_shrinks shrink_pass.deletions += size - len(self.shrink_target.choices) self.engine.clear_call_explanation() return True def fixate_shrink_passes(self, passes: list[ShrinkPass]) -> None: """Run steps from each pass in ``passes`` until the current shrink target is a fixed point of all of them.""" any_ran = True while any_ran: any_ran = False reordering = {} # We run remove_discarded after every pass to do cleanup # keeping track of whether that actually works. Either there is # no discarded data and it is basically free, or it reliably works # and deletes data, or it doesn't work. In that latter case we turn # it off for the rest of this loop through the passes, but will # try again once all of the passes have been run. can_discard = self.remove_discarded() calls_at_loop_start = self.calls # We keep track of how many calls can be made by a single step # without making progress and use this to test how much to pad # out self.max_stall by as we go along. max_calls_per_failing_step = 1 for sp in passes: if can_discard: can_discard = self.remove_discarded() before_sp = self.shrink_target # Run the shrink pass until it fails to make any progress # max_failures times in a row. This implicitly boosts shrink # passes that are more likely to work. failures = 0 max_failures = 20 while failures < max_failures: # We don't allow more than max_stall consecutive failures # to shrink, but this means that if we're unlucky and the # shrink passes are in a bad order where only the ones at # the end are useful, if we're not careful this heuristic # might stop us before we've tried everything. In order to # avoid that happening, we make sure that there's always # plenty of breathing room to make it through a single # iteration of the fixate_shrink_passes loop. self.max_stall = max( self.max_stall, 2 * max_calls_per_failing_step + (self.calls - calls_at_loop_start), ) prev = self.shrink_target initial_calls = self.calls # It's better for us to run shrink passes in a deterministic # order, to avoid repeat work, but this can cause us to create # long stalls when there are a lot of steps which fail to do # anything useful. In order to avoid this, once we've noticed # we're in a stall (i.e. half of max_failures calls have failed # to do anything) we switch to randomly jumping around. If we # find a success then we'll resume deterministic order from # there which, with any luck, is in a new good region. if not self.step(sp, random_order=failures >= max_failures // 2): # step returns False when there is nothing to do because # the entire choice tree is exhausted. If this happens # we break because we literally can't run this pass any # more than we already have until something else makes # progress. break any_ran = True # Don't count steps that didn't actually try to do # anything as failures. Otherwise, this call is a failure # if it failed to make any changes to the shrink target. if initial_calls != self.calls: if prev is not self.shrink_target: failures = 0 else: max_calls_per_failing_step = max( max_calls_per_failing_step, self.calls - initial_calls ) failures += 1 # We reorder the shrink passes so that on our next run through # we try good ones first. The rule is that shrink passes that # did nothing useful are the worst, shrink passes that reduced # the length are the best. if self.shrink_target is before_sp: reordering[sp] = 1 elif len(self.choices) < len(before_sp.choices): reordering[sp] = -1 else: reordering[sp] = 0 passes.sort(key=reordering.__getitem__) @property def nodes(self) -> tuple[ChoiceNode, ...]: return self.shrink_target.nodes @property def choices(self) -> tuple[ChoiceT, ...]: return self.shrink_target.choices @property def spans(self) -> Spans: return self.shrink_target.spans @derived_value # type: ignore def spans_by_label(self): """ A mapping of labels to a list of spans with that label. Spans in the list are ordered by their normal index order. """ spans_by_label = defaultdict(list) for ex in self.spans: spans_by_label[ex.label].append(ex) return dict(spans_by_label) @derived_value # type: ignore def distinct_labels(self): return sorted(self.spans_by_label, key=str) def pass_to_descendant(self, chooser): """Attempt to replace each span with a descendant span. This is designed to deal with strategies that call themselves recursively. For example, suppose we had: binary_tree = st.deferred( lambda: st.one_of( st.integers(), st.tuples(binary_tree, binary_tree))) This pass guarantees that we can replace any binary tree with one of its subtrees - each of those will create an interval that the parent could validly be replaced with, and this pass will try doing that. This is pretty expensive - it takes O(len(intervals)^2) - so we run it late in the process when we've got the number of intervals as far down as possible. """ label = chooser.choose( self.distinct_labels, lambda l: len(self.spans_by_label[l]) >= 2 ) spans = self.spans_by_label[label] i = chooser.choose(range(len(spans) - 1)) ancestor = spans[i] if i + 1 == len(spans) or spans[i + 1].start >= ancestor.end: return @self.cached(label, i) def descendants(): lo = i + 1 hi = len(spans) while lo + 1 < hi: mid = (lo + hi) // 2 if spans[mid].start >= ancestor.end: hi = mid else: lo = mid return [ span for span in spans[i + 1 : hi] if span.choice_count < ancestor.choice_count ] descendant = chooser.choose(descendants, lambda ex: ex.choice_count > 0) assert ancestor.start <= descendant.start assert ancestor.end >= descendant.end assert descendant.choice_count < ancestor.choice_count self.consider_new_nodes( self.nodes[: ancestor.start] + self.nodes[descendant.start : descendant.end] + self.nodes[ancestor.end :] ) def lower_common_node_offset(self): """Sometimes we find ourselves in a situation where changes to one part of the choice sequence unlock changes to other parts. Sometimes this is good, but sometimes this can cause us to exhibit exponential slow downs! e.g. suppose we had the following: m = draw(integers(min_value=0)) n = draw(integers(min_value=0)) assert abs(m - n) > 1 If this fails then we'll end up with a loop where on each iteration we reduce each of m and n by 2 - m can't go lower because of n, then n can't go lower because of m. This will take us O(m) iterations to complete, which is exponential in the data size, as we gradually zig zag our way towards zero. This can only happen if we're failing to reduce the size of the choice sequence: The number of iterations that reduce the length of the choice sequence is bounded by that length. So what we do is this: We keep track of which nodes are changing, and then if there's some non-zero common offset to them we try and minimize them all at once by lowering that offset. This may not work, and it definitely won't get us out of all possible exponential slow downs (an example of where it doesn't is where the shape of the nodes changes as a result of this bouncing behaviour), but it fails fast when it doesn't work and gets us out of a really nastily slow case when it does. """ if len(self.__changed_nodes) <= 1: return changed = [] for i in sorted(self.__changed_nodes): node = self.nodes[i] if node.trivial or node.type != "integer": continue changed.append(node) if not changed: return ints = [ abs(node.value - node.constraints["shrink_towards"]) for node in changed ] offset = min(ints) assert offset > 0 for i in range(len(ints)): ints[i] -= offset st = self.shrink_target def offset_node(node, n): return ( node.index, node.index + 1, [node.copy(with_value=node.constraints["shrink_towards"] + n)], ) def consider(n, sign): return self.consider_new_nodes( replace_all( st.nodes, [ offset_node(node, sign * (n + v)) for node, v in zip(changed, ints, strict=False) ], ) ) # shrink from both sides Integer.shrink(offset, lambda n: consider(n, 1)) Integer.shrink(offset, lambda n: consider(n, -1)) self.clear_change_tracking() def clear_change_tracking(self): self.__last_checked_changed_at = self.shrink_target self.__all_changed_nodes = set() def mark_changed(self, i): self.__changed_nodes.add(i) @property def __changed_nodes(self) -> set[int]: if self.__last_checked_changed_at is self.shrink_target: return self.__all_changed_nodes prev_target = self.__last_checked_changed_at new_target = self.shrink_target assert prev_target is not new_target prev_nodes = prev_target.nodes new_nodes = new_target.nodes assert sort_key(new_target.nodes) < sort_key(prev_target.nodes) if len(prev_nodes) != len(new_nodes) or any( n1.type != n2.type for n1, n2 in zip(prev_nodes, new_nodes, strict=True) ): # should we check constraints are equal as well? self.__all_changed_nodes = set() else: assert len(prev_nodes) == len(new_nodes) for i, (n1, n2) in enumerate(zip(prev_nodes, new_nodes, strict=True)): assert n1.type == n2.type if not choice_equal(n1.value, n2.value): self.__all_changed_nodes.add(i) return self.__all_changed_nodes def update_shrink_target(self, new_target): assert isinstance(new_target, ConjectureResult) self.shrinks += 1 # If we are just taking a long time to shrink we don't want to # trigger this heuristic, so whenever we shrink successfully # we give ourselves a bit of breathing room to make sure we # would find a shrink that took that long to find the next time. # The case where we're taking a long time but making steady # progress is handled by `finish_shrinking_deadline` in engine.py self.max_stall = max( self.max_stall, (self.calls - self.calls_at_last_shrink) * 2 ) self.calls_at_last_shrink = self.calls self.shrink_target = new_target self.__derived_values = {} def try_shrinking_nodes(self, nodes, n): """Attempts to replace each node in the nodes list with n. Returns True if it succeeded (which may include some additional modifications to shrink_target). In current usage it is expected that each of the nodes currently have the same value and choice_type, although this is not essential. Note that n must be < the node at min(nodes) or this is not a valid shrink. This method will attempt to do some small amount of work to delete data that occurs after the end of the nodes. This is useful for cases where there is some size dependency on the value of a node. """ # If the length of the shrink target has changed from under us such that # the indices are out of bounds, give up on the replacement. # TODO_BETTER_SHRINK: we probably want to narrow down the root cause here at some point. if any(node.index >= len(self.nodes) for node in nodes): return # pragma: no cover initial_attempt = replace_all( self.nodes, [(node.index, node.index + 1, [node.copy(with_value=n)]) for node in nodes], ) attempt = self.cached_test_function(initial_attempt)[1] if attempt is None: return False if attempt is self.shrink_target: # if the initial shrink was a success, try lowering offsets. self.lower_common_node_offset() return True # If this produced something completely invalid we ditch it # here rather than trying to persevere. if attempt.status is Status.OVERRUN: return False if attempt.status is Status.INVALID: return False if attempt.misaligned_at is not None: # we're invalid due to a misalignment in the tree. We'll try to fix # a very specific type of misalignment here: where we have a node of # {"size": n} and tried to draw the same node, but with {"size": m < n}. # This can occur with eg # # n = data.draw_integer() # s = data.draw_string(min_size=n) # # where we try lowering n, resulting in the test_function drawing a lower # min_size than our attempt had for the draw_string node. # # We'll now try realigning this tree by: # * replacing the constraints in our attempt with what test_function tried # to draw in practice # * truncating the value of that node to match min_size # # This helps in the specific case of drawing a value and then drawing # a collection of that size...and not much else. In practice this # helps because this antipattern is fairly common. # TODO we'll probably want to apply the same trick as in the valid # case of this function of preserving from the right instead of # preserving from the left. see test_can_shrink_variable_string_draws. index, attempt_choice_type, attempt_constraints, _attempt_forced = ( attempt.misaligned_at ) node = self.nodes[index] if node.type != attempt_choice_type: return False # pragma: no cover if node.was_forced: return False # pragma: no cover if node.type in {"string", "bytes"}: # if the size *increased*, we would have to guess what to pad with # in order to try fixing up this attempt. Just give up. if node.constraints["min_size"] <= attempt_constraints["min_size"]: # attempts which increase min_size tend to overrun rather than # be misaligned, making a covering case difficult. return False # pragma: no cover # the size decreased in our attempt. Try again, but truncate the value # to that size by removing any elements past min_size. return self.consider_new_nodes( initial_attempt[: node.index] + [ initial_attempt[node.index].copy( with_constraints=attempt_constraints, with_value=initial_attempt[node.index].value[ : attempt_constraints["min_size"] ], ) ] + initial_attempt[node.index :] ) lost_nodes = len(self.nodes) - len(attempt.nodes) if lost_nodes <= 0: return False start = nodes[0].index end = nodes[-1].index + 1 # We now look for contiguous regions to delete that might help fix up # this failed shrink. We only look for contiguous regions of the right # lengths because doing anything more than that starts to get very # expensive. See minimize_individual_choices for where we # try to be more aggressive. regions_to_delete = {(end, end + lost_nodes)} for ex in self.spans: if ex.start > start: continue if ex.end <= end: continue if ex.index >= len(attempt.spans): continue # pragma: no cover replacement = attempt.spans[ex.index] in_original = [c for c in ex.children if c.start >= end] in_replaced = [c for c in replacement.children if c.start >= end] if len(in_replaced) >= len(in_original) or not in_replaced: continue # We've found a span where some of the children went missing # as a result of this change, and just replacing it with the data # it would have had and removing the spillover didn't work. This # means that some of its children towards the right must be # important, so we try to arrange it so that it retains its # rightmost children instead of its leftmost. regions_to_delete.add( (in_original[0].start, in_original[-len(in_replaced)].start) ) for u, v in sorted(regions_to_delete, key=lambda x: x[1] - x[0], reverse=True): try_with_deleted = initial_attempt[:u] + initial_attempt[v:] if self.consider_new_nodes(try_with_deleted): return True return False def remove_discarded(self): """Try removing all bytes marked as discarded. This is primarily to deal with data that has been ignored while doing rejection sampling - e.g. as a result of an integer range, or a filtered strategy. Such data will also be handled by the adaptive_example_deletion pass, but that pass is necessarily more conservative and will try deleting each interval individually. The common case is that all data drawn and rejected can just be thrown away immediately in one block, so this pass will be much faster than trying each one individually when it works. returns False if there is discarded data and removing it does not work, otherwise returns True. """ while self.shrink_target.has_discards: discarded = [] for ex in self.shrink_target.spans: if ( ex.choice_count > 0 and ex.discarded and (not discarded or ex.start >= discarded[-1][-1]) ): discarded.append((ex.start, ex.end)) # This can happen if we have discards but they are all of # zero length. This shouldn't happen very often so it's # faster to check for it here than at the point of example # generation. if not discarded: break attempt = list(self.nodes) for u, v in reversed(discarded): del attempt[u:v] if not self.consider_new_nodes(tuple(attempt)): return False return True @derived_value # type: ignore def duplicated_nodes(self): """Returns a list of nodes grouped (choice_type, value).""" duplicates = defaultdict(list) for node in self.nodes: duplicates[(node.type, choice_key(node.value))].append(node) return list(duplicates.values()) def node_program(self, program: str) -> ShrinkPass: return ShrinkPass( lambda chooser: self._node_program(chooser, program), name=f"node_program_{program}", ) def _node_program(self, chooser, program): n = len(program) # Adaptively attempt to run the node program at the current # index. If this successfully applies the node program ``k`` times # then this runs in ``O(log(k))`` test function calls. i = chooser.choose(range(len(self.nodes) - n + 1)) # First, run the node program at the chosen index. If this fails, # don't do any extra work, so that failure is as cheap as possible. if not self.run_node_program(i, program, original=self.shrink_target): return # Because we run in a random order we will often find ourselves in the middle # of a region where we could run the node program. We thus start by moving # left to the beginning of that region if possible in order to start from # the beginning of that region. def offset_left(k): return i - k * n i = offset_left( find_integer( lambda k: self.run_node_program( offset_left(k), program, original=self.shrink_target ) ) ) original = self.shrink_target # Now try to run the node program multiple times here. find_integer( lambda k: self.run_node_program(i, program, original=original, repeats=k) ) def minimize_duplicated_choices(self, chooser): """Find choices that have been duplicated in multiple places and attempt to minimize all of the duplicates simultaneously. This lets us handle cases where two values can't be shrunk independently of each other but can easily be shrunk together. For example if we had something like: ls = data.draw(lists(integers())) y = data.draw(integers()) assert y not in ls Suppose we drew y = 3 and after shrinking we have ls = [3]. If we were to replace both 3s with 0, this would be a valid shrink, but if we were to replace either 3 with 0 on its own the test would start passing. It is also useful for when that duplication is accidental and the value of the choices don't matter very much because it allows us to replace more values at once. """ nodes = chooser.choose(self.duplicated_nodes) # we can't lower any nodes which are trivial. try proceeding with the # remaining nodes. nodes = [node for node in nodes if not node.trivial] if len(nodes) <= 1: return self.minimize_nodes(nodes) def redistribute_numeric_pairs(self, chooser): """If there is a sum of generated numbers that we need their sum to exceed some bound, lowering one of them requires raising the other. This pass enables that.""" # look for a pair of nodes (node1, node2) which are both numeric # and aren't separated by too many other nodes. We'll decrease node1 and # increase node2 (note that the other way around doesn't make sense as # it's strictly worse in the ordering). def can_choose_node(node): # don't choose nan, inf, or floats above the threshold where f + 1 > f # (which is not necessarily true for floats above MAX_PRECISE_INTEGER). # The motivation for the last condition is to avoid trying weird # non-shrinks where we raise one node and think we lowered another # (but didn't). return node.type in {"integer", "float"} and not ( node.type == "float" and (math.isnan(node.value) or abs(node.value) >= MAX_PRECISE_INTEGER) ) node1 = chooser.choose( self.nodes, lambda node: can_choose_node(node) and not node.trivial, ) node2 = chooser.choose( self.nodes, lambda node: can_choose_node(node) # Note that it's fine for node2 to be trivial, because we're going to # explicitly make it *not* trivial by adding to its value. and not node.was_forced # to avoid quadratic behavior, scan ahead only a small amount for # the related node. and node1.index < node.index <= node1.index + 4, ) m: int | float = node1.value n: int | float = node2.value def boost(k: int) -> bool: # floats always shrink towards 0 shrink_towards = ( node1.constraints["shrink_towards"] if node1.type == "integer" else 0 ) if k > abs(m - shrink_towards): return False # We are trying to move node1 (m) closer to shrink_towards, and node2 # (n) farther away from shrink_towards. If m is below shrink_towards, # we want to add to m and subtract from n, and vice versa if above # shrink_towards. if m < shrink_towards: k = -k try: v1 = m - k v2 = n + k except OverflowError: # pragma: no cover # if n or m is a float and k is over sys.float_info.max, coercing # k to a float will overflow. return False # if we've increased node2 to the point that we're past max precision, # give up - things have become too unstable. if node1.type == "float" and abs(v2) >= MAX_PRECISE_INTEGER: return False return self.consider_new_nodes( self.nodes[: node1.index] + (node1.copy(with_value=v1),) + self.nodes[node1.index + 1 : node2.index] + (node2.copy(with_value=v2),) + self.nodes[node2.index + 1 :] ) find_integer(boost) def lower_integers_together(self, chooser): node1 = chooser.choose( self.nodes, lambda n: n.type == "integer" and not n.trivial ) # Search up to 3 nodes ahead, to avoid quadratic time. node2 = self.nodes[ chooser.choose( range(node1.index + 1, min(len(self.nodes), node1.index + 3 + 1)), lambda i: self.nodes[i].type == "integer" and not self.nodes[i].was_forced, ) ] # one might expect us to require node2 to be nontrivial, and to minimize # the node which is closer to its shrink_towards, rather than node1 # unconditionally. In reality, it's acceptable for us to transition node2 # from trivial to nontrivial, because the shrink ordering is dominated by # the complexity of the earlier node1. What matters is minimizing node1. shrink_towards = node1.constraints["shrink_towards"] def consider(n): return self.consider_new_nodes( self.nodes[: node1.index] + (node1.copy(with_value=node1.value - n),) + self.nodes[node1.index + 1 : node2.index] + (node2.copy(with_value=node2.value - n),) + self.nodes[node2.index + 1 :] ) find_integer(lambda n: consider(shrink_towards - n)) find_integer(lambda n: consider(n - shrink_towards)) def lower_duplicated_characters(self, chooser): """ Select two string choices no more than 4 choices apart and simultaneously lower characters which appear in both strings. This helps cases where the same character must appear in two strings, but the actual value of the character is not relevant. This shrinking pass currently only tries lowering *all* instances of the duplicated character in both strings. So for instance, given two choices: "bbac" "abbb" we would try lowering all five of the b characters simultaneously. This may fail to shrink some cases where only certain character indices are correlated, for instance if only the b at index 1 could be lowered simultaneously and the rest did in fact actually have to be a `b`. It would be nice to try shrinking that case as well, but we would need good safeguards because it could get very expensive to try all combinations. I expect lowering all duplicates to handle most cases in the meantime. """ node1 = chooser.choose( self.nodes, lambda n: n.type == "string" and not n.trivial ) # limit search to up to 4 choices ahead, to avoid quadratic behavior node2 = self.nodes[ chooser.choose( range(node1.index + 1, min(len(self.nodes), node1.index + 1 + 4)), lambda i: self.nodes[i].type == "string" and not self.nodes[i].trivial # select nodes which have at least one of the same character present and set(node1.value) & set(self.nodes[i].value), ) ] duplicated_characters = set(node1.value) & set(node2.value) # deterministic ordering char = chooser.choose(sorted(duplicated_characters)) intervals = node1.constraints["intervals"] def copy_node(node, n): # replace all duplicate characters in each string. This might miss # some shrinks compared to only replacing some, but trying all possible # combinations of indices could get expensive if done without some # thought. return node.copy( with_value=node.value.replace(char, intervals.char_in_shrink_order(n)) ) Integer.shrink( intervals.index_from_char_in_shrink_order(char), lambda n: self.consider_new_nodes( self.nodes[: node1.index] + (copy_node(node1, n),) + self.nodes[node1.index + 1 : node2.index] + (copy_node(node2, n),) + self.nodes[node2.index + 1 :] ), ) def minimize_nodes(self, nodes): choice_type = nodes[0].type value = nodes[0].value # unlike choice_type and value, constraints are *not* guaranteed to be equal among all # passed nodes. We arbitrarily use the constraints of the first node. I think # this is unsound (= leads to us trying shrinks that could not have been # generated), but those get discarded at test-time, and this enables useful # slips where constraints are not equal but are close enough that doing the # same operation on both basically just works. constraints = nodes[0].constraints assert all( node.type == choice_type and choice_equal(node.value, value) for node in nodes ) if choice_type == "integer": shrink_towards = constraints["shrink_towards"] # try shrinking from both sides towards shrink_towards. # we're starting from n = abs(shrink_towards - value). Because the # shrinker will not check its starting value, we need to try # shrinking to n first. self.try_shrinking_nodes(nodes, abs(shrink_towards - value)) Integer.shrink( abs(shrink_towards - value), lambda n: self.try_shrinking_nodes(nodes, shrink_towards + n), ) Integer.shrink( abs(shrink_towards - value), lambda n: self.try_shrinking_nodes(nodes, shrink_towards - n), ) elif choice_type == "float": self.try_shrinking_nodes(nodes, abs(value)) Float.shrink( abs(value), lambda val: self.try_shrinking_nodes(nodes, val), ) Float.shrink( abs(value), lambda val: self.try_shrinking_nodes(nodes, -val), ) elif choice_type == "boolean": # must be True, otherwise would be trivial and not selected. assert value is True # only one thing to try: false! self.try_shrinking_nodes(nodes, False) elif choice_type == "bytes": Bytes.shrink( value, lambda val: self.try_shrinking_nodes(nodes, val), min_size=constraints["min_size"], ) elif choice_type == "string": String.shrink( value, lambda val: self.try_shrinking_nodes(nodes, val), intervals=constraints["intervals"], min_size=constraints["min_size"], ) else: raise NotImplementedError def try_trivial_spans(self, chooser): i = chooser.choose(range(len(self.spans))) prev = self.shrink_target nodes = self.shrink_target.nodes span = self.spans[i] prefix = nodes[: span.start] replacement = tuple( [ ( node if node.was_forced else node.copy( with_value=choice_from_index(0, node.type, node.constraints) ) ) for node in nodes[span.start : span.end] ] ) suffix = nodes[span.end :] attempt = self.cached_test_function(prefix + replacement + suffix)[1] if self.shrink_target is not prev: return if isinstance(attempt, ConjectureResult): new_span = attempt.spans[i] new_replacement = attempt.nodes[new_span.start : new_span.end] self.consider_new_nodes(prefix + new_replacement + suffix) def minimize_individual_choices(self, chooser): """Attempt to minimize each choice in sequence. This is the pass that ensures that e.g. each integer we draw is a minimum value. So it's the part that guarantees that if we e.g. do x = data.draw(integers()) assert x < 10 then in our shrunk example, x = 10 rather than say 97. If we are unsuccessful at minimizing a choice of interest we then check if that's because it's changing the size of the test case and, if so, we also make an attempt to delete parts of the test case to see if that fixes it. We handle most of the common cases in try_shrinking_nodes which is pretty good at clearing out large contiguous blocks of dead space, but it fails when there is data that has to stay in particular places in the list. """ node = chooser.choose(self.nodes, lambda node: not node.trivial) initial_target = self.shrink_target self.minimize_nodes([node]) if self.shrink_target is not initial_target: # the shrink target changed, so our shrink worked. Defer doing # anything more intelligent until this shrink fails. return # the shrink failed. One particularly common case where minimizing a # node can fail is the antipattern of drawing a size and then drawing a # collection of that size, or more generally when there is a size # dependency on some single node. We'll explicitly try and fix up this # common case here: if decreasing an integer node by one would reduce # the size of the generated input, we'll try deleting things after that # node and see if the resulting attempt works. if node.type != "integer": # Only try this fixup logic on integer draws. Almost all size # dependencies are on integer draws, and if it's not, it's doing # something convoluted enough that it is unlikely to shrink well anyway. # TODO: extent to floats? we probably currently fail on the following, # albeit convoluted example: # n = int(data.draw(st.floats())) # s = data.draw(st.lists(st.integers(), min_size=n, max_size=n)) return lowered = ( self.nodes[: node.index] + (node.copy(with_value=node.value - 1),) + self.nodes[node.index + 1 :] ) attempt = self.cached_test_function(lowered)[1] if ( attempt is None or attempt.status < Status.VALID or len(attempt.nodes) == len(self.nodes) or len(attempt.nodes) == node.index + 1 ): # no point in trying our size-dependency-logic if our attempt at # lowering the node resulted in: # * an invalid conjecture data # * the same number of nodes as before # * no nodes beyond the lowered node (nothing to try to delete afterwards) return # If it were then the original shrink should have worked and we could # never have got here. assert attempt is not self.shrink_target @self.cached(node.index) def first_span_after_node(): lo = 0 hi = len(self.spans) while lo + 1 < hi: mid = (lo + hi) // 2 span = self.spans[mid] if span.start >= node.index: hi = mid else: lo = mid return hi # we try deleting both entire spans, and single nodes. # If we wanted to get more aggressive, we could try deleting n # consecutive nodes (that don't cross a span boundary) for say # n <= 2 or n <= 3. if chooser.choose([True, False]): span = self.spans[ chooser.choose( range(first_span_after_node, len(self.spans)), lambda i: self.spans[i].choice_count > 0, ) ] self.consider_new_nodes(lowered[: span.start] + lowered[span.end :]) else: node = self.nodes[chooser.choose(range(node.index + 1, len(self.nodes)))] self.consider_new_nodes(lowered[: node.index] + lowered[node.index + 1 :]) def reorder_spans(self, chooser): """This pass allows us to reorder the children of each span. For example, consider the following: .. code-block:: python import hypothesis.strategies as st from hypothesis import given @given(st.text(), st.text()) def test_not_equal(x, y): assert x != y Without the ability to reorder x and y this could fail either with ``x=""``, ``y="0"``, or the other way around. With reordering it will reliably fail with ``x=""``, ``y="0"``. """ span = chooser.choose(self.spans) label = chooser.choose(span.children).label spans = [c for c in span.children if c.label == label] if len(spans) <= 1: return endpoints = [(span.start, span.end) for span in spans] st = self.shrink_target Ordering.shrink( range(len(spans)), lambda indices: self.consider_new_nodes( replace_all( st.nodes, [ ( u, v, st.nodes[spans[i].start : spans[i].end], ) for (u, v), i in zip(endpoints, indices, strict=True) ], ) ), key=lambda i: sort_key(st.nodes[spans[i].start : spans[i].end]), ) def run_node_program(self, i, program, original, repeats=1): """Node programs are a mini-DSL for node rewriting, defined as a sequence of commands that can be run at some index into the nodes Commands are: * "X", delete this node This method runs the node program in ``program`` at node index ``i`` on the ConjectureData ``original``. If ``repeats > 1`` then it will attempt to approximate the results of running it that many times. Returns True if this successfully changes the underlying shrink target, else False. """ if i + len(program) > len(original.nodes) or i < 0: return False attempt = list(original.nodes) for _ in range(repeats): for k, command in reversed(list(enumerate(program))): j = i + k if j >= len(attempt): return False if command == "X": del attempt[j] else: raise NotImplementedError(f"Unrecognised command {command!r}") return self.consider_new_nodes(attempt) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.internal.conjecture.shrinking.bytes import Bytes from hypothesis.internal.conjecture.shrinking.collection import Collection from hypothesis.internal.conjecture.shrinking.floats import Float from hypothesis.internal.conjecture.shrinking.integer import Integer from hypothesis.internal.conjecture.shrinking.ordering import Ordering from hypothesis.internal.conjecture.shrinking.string import String __all__ = ["Bytes", "Collection", "Float", "Integer", "Ordering", "String"] ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/bytes.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.internal.conjecture.shrinking.collection import Collection from hypothesis.internal.conjecture.shrinking.integer import Integer class Bytes(Collection): def __init__(self, initial, predicate, **kwargs): super().__init__( # implicit conversion from bytes to list of integers here list(initial), lambda val: predicate(bytes(val)), ElementShrinker=Integer, **kwargs, ) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/choicetree.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import defaultdict from collections.abc import Callable, Iterable, Sequence from random import Random from hypothesis.internal.conjecture.junkdrawer import LazySequenceCopy def prefix_selection_order( prefix: Sequence[int], ) -> Callable[[int, int], Iterable[int]]: """Select choices starting from ``prefix```, preferring to move left then wrapping around to the right.""" def selection_order(depth: int, n: int) -> Iterable[int]: if depth < len(prefix): i = prefix[depth] if i >= n: i = n - 1 yield from range(i, -1, -1) yield from range(n - 1, i, -1) else: yield from range(n - 1, -1, -1) return selection_order def random_selection_order(random: Random) -> Callable[[int, int], Iterable[int]]: """Select choices uniformly at random.""" def selection_order(depth: int, n: int) -> Iterable[int]: pending = LazySequenceCopy(range(n)) while pending: i = random.randrange(0, len(pending)) yield pending.pop(i) return selection_order class Chooser: """A source of nondeterminism for use in shrink passes.""" def __init__( self, tree: "ChoiceTree", selection_order: Callable[[int, int], Iterable[int]], ): self.__selection_order = selection_order self.__node_trail = [tree.root] self.__choices: list[int] = [] self.__finished = False def choose( self, values: Sequence[int], condition: Callable[[int], bool] = lambda x: True, ) -> int: """Return some element of values satisfying the condition that will not lead to an exhausted branch, or raise DeadBranch if no such element exist". """ assert not self.__finished node = self.__node_trail[-1] if node.live_child_count is None: node.live_child_count = len(values) node.n = len(values) assert node.live_child_count > 0 or len(values) == 0 for i in self.__selection_order(len(self.__choices), len(values)): if node.live_child_count == 0: break if not node.children[i].exhausted: v = values[i] if condition(v): self.__choices.append(i) self.__node_trail.append(node.children[i]) return v else: node.children[i] = DeadNode node.live_child_count -= 1 assert node.live_child_count == 0 raise DeadBranch def finish(self) -> Sequence[int]: """Record the decisions made in the underlying tree and return a prefix that can be used for the next Chooser to be used.""" self.__finished = True assert len(self.__node_trail) == len(self.__choices) + 1 result = tuple(self.__choices) self.__node_trail[-1].live_child_count = 0 while len(self.__node_trail) > 1 and self.__node_trail[-1].exhausted: self.__node_trail.pop() assert len(self.__node_trail) == len(self.__choices) i = self.__choices.pop() target = self.__node_trail[-1] target.children[i] = DeadNode assert target.live_child_count is not None target.live_child_count -= 1 return result class ChoiceTree: """Records sequences of choices made during shrinking so that we can track what parts of a pass has run. Used to create Chooser objects that are the main interface that a pass uses to make decisions about what to do. """ def __init__(self) -> None: self.root = TreeNode() @property def exhausted(self) -> bool: return self.root.exhausted def step( self, selection_order: Callable[[int, int], Iterable[int]], f: Callable[[Chooser], None], ) -> Sequence[int]: assert not self.exhausted chooser = Chooser(self, selection_order) try: f(chooser) except DeadBranch: pass return chooser.finish() class TreeNode: def __init__(self) -> None: self.children: dict[int, TreeNode] = defaultdict(TreeNode) self.live_child_count: int | None = None self.n: int | None = None @property def exhausted(self) -> bool: return self.live_child_count == 0 DeadNode = TreeNode() DeadNode.live_child_count = 0 class DeadBranch(Exception): pass ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/collection.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import Counter from hypothesis.internal.conjecture.shrinking.common import Shrinker from hypothesis.internal.conjecture.shrinking.ordering import Ordering from hypothesis.internal.conjecture.utils import identity class Collection(Shrinker): def setup( self, *, ElementShrinker, min_size, to_order=identity, from_order=identity ): self.ElementShrinker = ElementShrinker self.to_order = to_order self.from_order = from_order self.min_size = min_size def make_immutable(self, value): return tuple(value) def short_circuit(self): zero = self.from_order(0) return self.consider([zero] * self.min_size) def left_is_better(self, left, right): if len(left) < len(right): return True # examine elements one by one from the left until an element differs. for v1, v2 in zip(left, right, strict=False): if self.to_order(v1) == self.to_order(v2): continue return self.to_order(v1) < self.to_order(v2) # equal length and all values were equal by our ordering, so must be equal # by our ordering. assert list(map(self.to_order, left)) == list(map(self.to_order, right)) return False def run_step(self): # try all-zero first; we already considered all-zero-and-smallest in # short_circuit. zero = self.from_order(0) self.consider([zero] * len(self.current)) # try deleting each element in turn, starting from the back # TODO_BETTER_SHRINK: adaptively delete here by deleting larger chunks at once # if early deletes succeed. use find_integer. turns O(n) into O(log(n)) for i in reversed(range(len(self.current))): self.consider(self.current[:i] + self.current[i + 1 :]) # then try reordering Ordering.shrink(self.current, self.consider, key=self.to_order) # then try minimizing all duplicated elements together simultaneously. This # helps in cases like https://github.com/HypothesisWorks/hypothesis/issues/4286 duplicated = {val for val, count in Counter(self.current).items() if count > 1} for val in duplicated: self.ElementShrinker.shrink( self.to_order(val), lambda v: self.consider( tuple(self.from_order(v) if x == val else x for x in self.current) ), ) # then try minimizing each element in turn for i, val in enumerate(self.current): self.ElementShrinker.shrink( self.to_order(val), lambda v: self.consider( self.current[:i] + (self.from_order(v),) + self.current[i + 1 :] ), ) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This module implements various useful common functions for shrinking tasks.""" class Shrinker: """A Shrinker object manages a single value and a predicate it should satisfy, and attempts to improve it in some direction, making it smaller and simpler.""" def __init__( self, initial, predicate, *, full=False, debug=False, name=None, **kwargs, ): self.setup(**kwargs) self.current = self.make_immutable(initial) self.initial = self.current self.full = full self.changes = 0 self.name = name self.__predicate = predicate self.__seen = {self.make_canonical(self.current)} self.debugging_enabled = debug @property def calls(self) -> int: return len(self.__seen) def __repr__(self) -> str: return "{}({}initial={!r}, current={!r})".format( type(self).__name__, "" if self.name is None else f"{self.name!r}, ", self.initial, self.current, ) def setup(self, **kwargs): """Runs initial setup code. Convenience function for children that doesn't require messing with the signature of init. """ def delegate(self, other_class, convert_to, convert_from, **kwargs): """Delegates shrinking to another shrinker class, by converting the current value to and from it with provided functions.""" self.call_shrinker( other_class, convert_to(self.current), lambda v: self.consider(convert_from(v)), **kwargs, ) def call_shrinker(self, other_class, initial, predicate, **kwargs): """Calls another shrinker class, passing through the relevant context variables. Note we explicitly do not pass through full. """ return other_class.shrink(initial, predicate, **kwargs) def debug(self, *args: object) -> None: if self.debugging_enabled: print("DEBUG", self, *args) @classmethod def shrink(cls, initial, predicate, **kwargs): """Shrink the value ``initial`` subject to the constraint that it satisfies ``predicate``. Returns the shrunk value. """ shrinker = cls(initial, predicate, **kwargs) shrinker.run() return shrinker.current def run(self): """Run for an appropriate number of steps to improve the current value. If self.full is True, will run until no further improvements can be found. """ if self.short_circuit(): return if self.full: prev = -1 while self.changes != prev: prev = self.changes self.run_step() else: self.run_step() self.debug("COMPLETE") def consider(self, value): """Try using ``value`` as a possible candidate improvement. Return True if self.current is canonically equal to value after the call, either because the value was incorporated as an improvement or because it had that value already. """ value = self.make_immutable(value) self.debug(f"considering {value!r}") canonical = self.make_canonical(value) if canonical == self.make_canonical(self.current): return True if canonical in self.__seen: return False self.__seen.add(canonical) self.check_invariants(value) if not self.left_is_better(value, self.current): self.debug(f"Rejected {value!r} as no better than {self.current=}") return False if self.__predicate(value): self.debug(f"shrinking to {value!r}") self.changes += 1 self.current = value return True else: self.debug(f"Rejected {value!r} not satisfying predicate") return False def make_canonical(self, value): """Convert immutable value into a canonical and hashable, but not necessarily equal, representation of itself. This representation is used only for tracking already-seen values, not passed to the shrinker. Defaults to just returning the (immutable) input value. """ return value def make_immutable(self, value): """Convert value into an immutable representation of itself. It is these immutable versions that the shrinker will work on. Defaults to just returning the value. """ return value def check_invariants(self, value): """Make appropriate assertions about the value to ensure that it is valid for this shrinker. Does nothing by default. """ def short_circuit(self): """Possibly attempt to do some shrinking. If this returns True, the ``run`` method will terminate early without doing any more work. """ return False def left_is_better(self, left, right): """Returns True if the left is strictly simpler than the right according to the standards of this shrinker.""" raise NotImplementedError def run_step(self): """Run a single step of the main shrink loop, attempting to improve the current value.""" raise NotImplementedError ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys from hypothesis.internal.conjecture.floats import float_to_lex from hypothesis.internal.conjecture.shrinking.common import Shrinker from hypothesis.internal.conjecture.shrinking.integer import Integer from hypothesis.internal.floats import MAX_PRECISE_INTEGER, float_to_int class Float(Shrinker): def setup(self): self.debugging_enabled = True def make_canonical(self, f): if math.isnan(f): # Distinguish different NaN bit patterns, while making each equal to itself. # Wrap in tuple to avoid potential collision with (huge) finite floats. return ("nan", float_to_int(f)) return f def check_invariants(self, value): # We only handle positive floats (including NaN) because we encode the sign # separately anyway. assert not (value < 0) def left_is_better(self, left, right): lex1 = float_to_lex(left) lex2 = float_to_lex(right) return lex1 < lex2 def short_circuit(self): # We check for a bunch of standard "large" floats. If we're currently # worse than them and the shrink downwards doesn't help, abort early # because there's not much useful we can do here. for g in [sys.float_info.max, math.inf, math.nan]: self.consider(g) # If we're stuck at a nasty float don't try to shrink it further. if not math.isfinite(self.current): return True def run_step(self): # above MAX_PRECISE_INTEGER, all floats are integers. Shrink like one. # TODO_BETTER_SHRINK: at 2 * MAX_PRECISE_INTEGER, n - 1 == n - 2, and # Integer.shrink will likely perform badly. We should have a specialized # big-float shrinker, which mostly follows Integer.shrink but replaces # n - 1 with next_down(n). if self.current > MAX_PRECISE_INTEGER: self.delegate(Integer, convert_to=int, convert_from=float) return # Finally we get to the important bit: Each of these is a small change # to the floating point number that corresponds to a large change in # the lexical representation. Trying these ensures that our floating # point shrink can always move past these obstacles. In particular it # ensures we can always move to integer boundaries and shrink past a # change that would require shifting the exponent while not changing # the float value much. # First, try dropping precision bits by rounding the scaled value. We # try values ordered from least-precise (integer) to more precise, ie. # approximate lexicographical order. Once we find an acceptable shrink, # self.consider discards the remaining attempts early and skips test # invocation. The loop count sets max fractional bits to keep, and is a # compromise between completeness and performance. for p in range(10): scaled = self.current * 2**p # note: self.current may change in loop for truncate in [math.floor, math.ceil]: self.consider(truncate(scaled) / 2**p) if self.consider(int(self.current)): self.debug("Just an integer now") self.delegate(Integer, convert_to=int, convert_from=float) return # Now try to minimize the top part of the fraction as an integer. This # basically splits the float as k + x with 0 <= x < 1 and minimizes # k as an integer, but without the precision issues that would have. m, n = self.current.as_integer_ratio() i, r = divmod(m, n) self.call_shrinker(Integer, i, lambda k: self.consider((k * n + r) / n)) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/integer.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.internal.conjecture.junkdrawer import find_integer from hypothesis.internal.conjecture.shrinking.common import Shrinker """ This module implements a shrinker for non-negative integers. """ class Integer(Shrinker): """Attempts to find a smaller integer. Guaranteed things to try ``0``, ``1``, ``initial - 1``, ``initial - 2``. Plenty of optimisations beyond that but those are the guaranteed ones. """ def short_circuit(self): for i in range(2): if self.consider(i): return True self.mask_high_bits() if self.size > 8: # see if we can squeeze the integer into a single byte. self.consider(self.current >> (self.size - 8)) self.consider(self.current & 0xFF) return self.current == 2 def check_invariants(self, value): assert value >= 0 def left_is_better(self, left, right): return left < right def run_step(self): self.shift_right() self.shrink_by_multiples(2) self.shrink_by_multiples(1) def shift_right(self): base = self.current find_integer(lambda k: k <= self.size and self.consider(base >> k)) def mask_high_bits(self): base = self.current n = base.bit_length() @find_integer def try_mask(k): if k >= n: return False mask = (1 << (n - k)) - 1 return self.consider(mask & base) @property def size(self) -> int: return self.current.bit_length() def shrink_by_multiples(self, k): base = self.current @find_integer def shrunk(n): attempt = base - n * k return attempt >= 0 and self.consider(attempt) return shrunk > 0 ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/ordering.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.internal.conjecture.junkdrawer import find_integer from hypothesis.internal.conjecture.shrinking.common import Shrinker from hypothesis.internal.conjecture.utils import identity class Ordering(Shrinker): """A shrinker that tries to make a sequence more sorted. Will not change the length or the contents, only tries to reorder the elements of the sequence. """ def setup(self, key=identity): self.key = key def make_immutable(self, value): return tuple(value) def short_circuit(self): # If we can flat out sort the target then there's nothing more to do. return self.consider(sorted(self.current, key=self.key)) def left_is_better(self, left, right): return tuple(map(self.key, left)) < tuple(map(self.key, right)) def check_invariants(self, value): assert len(value) == len(self.current) assert sorted(value) == sorted(self.current) def run_step(self): self.sort_regions() self.sort_regions_with_gaps() def sort_regions(self): """Guarantees that for each i we have tried to swap index i with index i + 1. This uses an adaptive algorithm that works by sorting contiguous regions starting from each element. """ i = 0 while i + 1 < len(self.current): prefix = list(self.current[:i]) k = find_integer( lambda k: i + k <= len(self.current) and self.consider( prefix + sorted(self.current[i : i + k], key=self.key) + list(self.current[i + k :]) ) ) i += k def sort_regions_with_gaps(self): """Guarantees that for each i we have tried to swap index i with index i + 2. This uses an adaptive algorithm that works by sorting contiguous regions centered on each element, where that element is treated as fixed and the elements around it are sorted.. """ for i in range(1, len(self.current) - 1): if self.current[i - 1] <= self.current[i] <= self.current[i + 1]: # The `continue` line is optimised out of the bytecode on # CPython >= 3.7 (https://bugs.python.org/issue2506) and on # PyPy, and so coverage cannot tell that it has been taken. continue # pragma: no cover def can_sort(a, b): if a < 0 or b > len(self.current): return False assert a <= i < b split = i - a values = sorted(self.current[a:i] + self.current[i + 1 : b]) return self.consider( list(self.current[:a]) + values[:split] + [self.current[i]] + values[split:] + list(self.current[b:]) ) left = i right = i + 1 right += find_integer(lambda k: can_sort(left, right + k)) find_integer(lambda k: can_sort(left - k, right)) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/shrinking/string.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.internal.conjecture.shrinking.collection import Collection from hypothesis.internal.conjecture.shrinking.integer import Integer class String(Collection): def __init__(self, initial, predicate, *, intervals, **kwargs): super().__init__( list(initial), lambda val: predicate("".join(val)), to_order=intervals.index_from_char_in_shrink_order, from_order=intervals.char_in_shrink_order, ElementShrinker=Integer, **kwargs, ) ================================================ FILE: hypothesis-python/src/hypothesis/internal/conjecture/utils.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import enum import hashlib import heapq import math import sys from collections import OrderedDict, abc from collections.abc import Callable, Sequence from functools import lru_cache from types import FunctionType from typing import TYPE_CHECKING, TypeVar from hypothesis.errors import InvalidArgument from hypothesis.internal.compat import int_from_bytes from hypothesis.internal.floats import next_up from hypothesis.internal.lambda_sources import _function_key if TYPE_CHECKING: from hypothesis.internal.conjecture.data import ConjectureData LABEL_MASK = 2**64 - 1 def calc_label_from_name(name: str) -> int: hashed = hashlib.sha384(name.encode()).digest() return int_from_bytes(hashed[:8]) def calc_label_from_callable(f: Callable) -> int: if isinstance(f, FunctionType): return calc_label_from_hash(_function_key(f, ignore_name=True)) elif isinstance(f, type): return calc_label_from_cls(f) else: # probably an instance defining __call__ try: return calc_label_from_hash(f) except Exception: # not hashable return calc_label_from_cls(type(f)) def calc_label_from_cls(cls: type) -> int: return calc_label_from_name(cls.__qualname__) def calc_label_from_hash(obj: object) -> int: return calc_label_from_name(str(hash(obj))) def combine_labels(*labels: int) -> int: label = 0 for l in labels: label = (label << 1) & LABEL_MASK label ^= l return label SAMPLE_IN_SAMPLER_LABEL = calc_label_from_name("a sample() in Sampler") ONE_FROM_MANY_LABEL = calc_label_from_name("one more from many()") T = TypeVar("T") def identity(v: T) -> T: return v def check_sample( values: type[enum.Enum] | Sequence[T], strategy_name: str ) -> Sequence[T]: if "numpy" in sys.modules and isinstance(values, sys.modules["numpy"].ndarray): if values.ndim != 1: raise InvalidArgument( "Only one-dimensional arrays are supported for sampling, " f"and the given value has {values.ndim} dimensions (shape " f"{values.shape}). This array would give samples of array slices " "instead of elements! Use np.ravel(values) to convert " "to a one-dimensional array, or tuple(values) if you " "want to sample slices." ) elif not isinstance(values, (OrderedDict, abc.Sequence, enum.EnumMeta)): raise InvalidArgument( f"Cannot sample from {values!r} because it is not an ordered collection. " f"Hypothesis goes to some length to ensure that the {strategy_name} " "strategy has stable results between runs. To replay a saved " "example, the sampled values must have the same iteration order " "on every run - ruling out sets, dicts, etc due to hash " "randomization. Most cases can simply use `sorted(values)`, but " "mixed types or special values such as math.nan require careful " "handling - and note that when simplifying an example, " "Hypothesis treats earlier values as simpler." ) if isinstance(values, range): # Pyright is unhappy with every way I've tried to type-annotate this # function, so fine, we'll just ignore the analysis error. return values # type: ignore return tuple(values) @lru_cache(64) def compute_sampler_table(weights: tuple[float, ...]) -> list[tuple[int, int, float]]: n = len(weights) table: list[list[int | float | None]] = [[i, None, None] for i in range(n)] total = sum(weights) num_type = type(total) zero = num_type(0) # type: ignore one = num_type(1) # type: ignore small: list[int] = [] large: list[int] = [] probabilities = [w / total for w in weights] scaled_probabilities: list[float] = [] for i, alternate_chance in enumerate(probabilities): scaled = alternate_chance * n scaled_probabilities.append(scaled) if scaled == 1: table[i][2] = zero elif scaled < 1: small.append(i) else: large.append(i) heapq.heapify(small) heapq.heapify(large) while small and large: lo = heapq.heappop(small) hi = heapq.heappop(large) assert lo != hi assert scaled_probabilities[hi] > one assert table[lo][1] is None table[lo][1] = hi table[lo][2] = one - scaled_probabilities[lo] scaled_probabilities[hi] = ( scaled_probabilities[hi] + scaled_probabilities[lo] ) - one if scaled_probabilities[hi] < 1: heapq.heappush(small, hi) elif scaled_probabilities[hi] == 1: table[hi][2] = zero else: heapq.heappush(large, hi) while large: table[large.pop()][2] = zero while small: table[small.pop()][2] = zero new_table: list[tuple[int, int, float]] = [] for base, alternate, alternate_chance in table: assert isinstance(base, int) assert isinstance(alternate, int) or alternate is None assert alternate_chance is not None if alternate is None: new_table.append((base, base, alternate_chance)) elif alternate < base: new_table.append((alternate, base, one - alternate_chance)) else: new_table.append((base, alternate, alternate_chance)) new_table.sort() return new_table class Sampler: """Sampler based on Vose's algorithm for the alias method. See http://www.keithschwarz.com/darts-dice-coins/ for a good explanation. The general idea is that we store a table of triples (base, alternate, p). base. We then pick a triple uniformly at random, and choose its alternate value with probability p and else choose its base value. The triples are chosen so that the resulting mixture has the right distribution. We maintain the following invariants to try to produce good shrinks: 1. The table is in lexicographic (base, alternate) order, so that choosing an earlier value in the list always lowers (or at least leaves unchanged) the value. 2. base[i] < alternate[i], so that shrinking the draw always results in shrinking the chosen element. """ table: list[tuple[int, int, float]] # (base_idx, alt_idx, alt_chance) def __init__(self, weights: Sequence[float], *, observe: bool = True): self.observe = observe self.table = compute_sampler_table(tuple(weights)) def sample( self, data: "ConjectureData", *, forced: int | None = None, ) -> int: if self.observe: data.start_span(SAMPLE_IN_SAMPLER_LABEL) forced_choice = ( # pragma: no branch # https://github.com/nedbat/coveragepy/issues/1617 None if forced is None else next( (base, alternate, alternate_chance) for (base, alternate, alternate_chance) in self.table if forced == base or (forced == alternate and alternate_chance > 0) ) ) base, alternate, alternate_chance = data.choice( self.table, forced=forced_choice, observe=self.observe, ) forced_use_alternate = None if forced is not None: # we maintain this invariant when picking forced_choice above. # This song and dance about alternate_chance > 0 is to avoid forcing # e.g. draw_boolean(p=0, forced=True), which is an error. forced_use_alternate = forced == alternate and alternate_chance > 0 assert forced == base or forced_use_alternate use_alternate = data.draw_boolean( alternate_chance, forced=forced_use_alternate, observe=self.observe, ) if self.observe: data.stop_span() if use_alternate: assert forced is None or alternate == forced, (forced, alternate) return alternate else: assert forced is None or base == forced, (forced, base) return base INT_SIZES = (8, 16, 32, 64, 128) INT_SIZES_SAMPLER = Sampler((4.0, 8.0, 1.0, 1.0, 0.5), observe=False) class many: """Utility class for collections. Bundles up the logic we use for "should I keep drawing more values?" and handles starting and stopping examples in the right place. Intended usage is something like: elements = many(data, ...) while elements.more(): add_stuff_to_result() """ def __init__( self, data: "ConjectureData", min_size: int, max_size: int | float, average_size: int | float, *, forced: int | None = None, observe: bool = True, ) -> None: assert 0 <= min_size <= average_size <= max_size assert forced is None or min_size <= forced <= max_size self.min_size = min_size self.max_size = max_size self.data = data self.forced_size = forced self.p_continue = _calc_p_continue(average_size - min_size, max_size - min_size) self.count = 0 self.rejections = 0 self.drawn = False self.force_stop = False self.rejected = False self.observe = observe def stop_span(self): if self.observe: self.data.stop_span() def start_span(self, label): if self.observe: self.data.start_span(label) def more(self) -> bool: """Should I draw another element to add to the collection?""" if self.drawn: self.stop_span() self.drawn = True self.rejected = False self.start_span(ONE_FROM_MANY_LABEL) if self.min_size == self.max_size: # if we have to hit an exact size, draw unconditionally until that # point, and no further. should_continue = self.count < self.min_size else: forced_result = None if self.force_stop: # if our size is forced, we can't reject in a way that would # cause us to differ from the forced size. assert self.forced_size is None or self.count == self.forced_size forced_result = False elif self.count < self.min_size: forced_result = True elif self.count >= self.max_size: forced_result = False elif self.forced_size is not None: forced_result = self.count < self.forced_size should_continue = self.data.draw_boolean( self.p_continue, forced=forced_result, observe=self.observe, ) if should_continue: self.count += 1 return True else: self.stop_span() return False def reject(self, why: str | None = None) -> None: """Reject the last example (i.e. don't count it towards our budget of elements because it's not going to go in the final collection).""" assert self.count > 0 self.count -= 1 self.rejections += 1 self.rejected = True # We set a minimum number of rejections before we give up to avoid # failing too fast when we reject the first draw. if self.rejections > max(3, 2 * self.count): if self.count < self.min_size: self.data.mark_invalid(why) else: self.force_stop = True SMALLEST_POSITIVE_FLOAT: float = next_up(0.0) or sys.float_info.min @lru_cache def _calc_p_continue(desired_avg: float, max_size: int | float) -> float: """Return the p_continue which will generate the desired average size.""" assert desired_avg <= max_size, (desired_avg, max_size) if desired_avg == max_size: return 1.0 p_continue = 1 - 1.0 / (1 + desired_avg) if p_continue == 0 or max_size == math.inf: assert 0 <= p_continue < 1, p_continue return p_continue assert 0 < p_continue < 1, p_continue # For small max_size, the infinite-series p_continue is a poor approximation, # and while we can't solve the polynomial a few rounds of iteration quickly # gets us a good approximate solution in almost all cases (sometimes exact!). while _p_continue_to_avg(p_continue, max_size) > desired_avg: # This is impossible over the reals, but *can* happen with floats. p_continue -= 0.0001 # If we've reached zero or gone negative, we want to break out of this loop, # and do so even if we're on a system with the unsafe denormals-are-zero flag. # We make that an explicit error in st.floats(), but here we'd prefer to # just get somewhat worse precision on collection lengths. if p_continue < SMALLEST_POSITIVE_FLOAT: p_continue = SMALLEST_POSITIVE_FLOAT break # Let's binary-search our way to a better estimate! We tried fancier options # like gradient descent, but this is numerically stable and works better. hi = 1.0 while desired_avg - _p_continue_to_avg(p_continue, max_size) > 0.01: assert 0 < p_continue < hi, (p_continue, hi) mid = (p_continue + hi) / 2 if _p_continue_to_avg(mid, max_size) <= desired_avg: p_continue = mid else: hi = mid assert 0 < p_continue < 1, p_continue assert _p_continue_to_avg(p_continue, max_size) <= desired_avg return p_continue def _p_continue_to_avg(p_continue: float, max_size: int | float) -> float: """Return the average_size generated by this p_continue and max_size.""" if p_continue >= 1: return max_size return (1.0 / (1 - p_continue) - 1) * (1 - p_continue**max_size) ================================================ FILE: hypothesis-python/src/hypothesis/internal/constants_ast.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import ast import hashlib import inspect import math import sys from ast import Constant, Expr, NodeVisitor, UnaryOp, USub from collections.abc import Iterator, MutableSet from functools import lru_cache from itertools import chain from pathlib import Path from types import ModuleType from typing import TypeAlias import hypothesis from hypothesis.configuration import storage_directory from hypothesis.internal.conjecture.choice import ChoiceTypeT from hypothesis.internal.escalation import is_hypothesis_file ConstantT: TypeAlias = int | float | bytes | str # unfortunate collision with builtin. I don't want to name the init arg bytes_. bytesT = bytes class Constants: def __init__( self, *, integers: MutableSet[int] | None = None, floats: MutableSet[float] | None = None, bytes: MutableSet[bytes] | None = None, strings: MutableSet[str] | None = None, ): self.integers: MutableSet[int] = set() if integers is None else integers self.floats: MutableSet[float] = set() if floats is None else floats self.bytes: MutableSet[bytesT] = set() if bytes is None else bytes self.strings: MutableSet[str] = set() if strings is None else strings def set_for_type( self, constant_type: type[ConstantT] | ChoiceTypeT ) -> MutableSet[int] | MutableSet[float] | MutableSet[bytes] | MutableSet[str]: if constant_type is int or constant_type == "integer": return self.integers elif constant_type is float or constant_type == "float": return self.floats elif constant_type is bytes or constant_type == "bytes": return self.bytes elif constant_type is str or constant_type == "string": return self.strings raise ValueError(f"unknown constant_type {constant_type}") def add(self, constant: ConstantT) -> None: self.set_for_type(type(constant)).add(constant) # type: ignore def __contains__(self, constant: ConstantT) -> bool: return constant in self.set_for_type(type(constant)) def __or__(self, other: "Constants") -> "Constants": return Constants( integers=self.integers | other.integers, # type: ignore floats=self.floats | other.floats, # type: ignore bytes=self.bytes | other.bytes, # type: ignore strings=self.strings | other.strings, # type: ignore ) def __iter__(self) -> Iterator[ConstantT]: return iter(chain(self.integers, self.floats, self.bytes, self.strings)) def __len__(self) -> int: return ( len(self.integers) + len(self.floats) + len(self.bytes) + len(self.strings) ) def __repr__(self) -> str: return f"Constants({self.integers=}, {self.floats=}, {self.bytes=}, {self.strings=})" def __eq__(self, other: object) -> bool: if not isinstance(other, Constants): return False return ( self.integers == other.integers and self.floats == other.floats and self.bytes == other.bytes and self.strings == other.strings ) class TooManyConstants(Exception): # a control flow exception which we raise in ConstantsVisitor when the # number of constants in a module gets too large. pass class ConstantVisitor(NodeVisitor): CONSTANTS_LIMIT: int = 1024 def __init__(self, *, limit: bool): super().__init__() self.constants = Constants() self.limit = limit def _add_constant(self, value: object) -> None: if self.limit and len(self.constants) >= self.CONSTANTS_LIMIT: raise TooManyConstants if isinstance(value, str) and ( value.isspace() or value == "" # long strings are unlikely to be useful. or len(value) > 20 ): return if isinstance(value, bytes) and ( value == b"" # long bytes seem plausibly more likely to be useful than long strings # (e.g. AES-256 has a 32 byte key), but we still want to cap at some # point to avoid performance issues. or len(value) > 50 ): return if isinstance(value, bool): return if isinstance(value, float) and math.isinf(value): # we already upweight inf. return if isinstance(value, int) and -100 < value < 100: # we already upweight small integers. return if isinstance(value, (int, float, bytes, str)): self.constants.add(value) return # I don't kow what case could go here, but am also not confident there # isn't one. return # pragma: no cover def visit_UnaryOp(self, node: UnaryOp) -> None: # `a = -1` is actually a combination of a USub and the constant 1. if ( isinstance(node.op, USub) and isinstance(node.operand, Constant) and isinstance(node.operand.value, (int, float)) and not isinstance(node.operand.value, bool) ): self._add_constant(-node.operand.value) # don't recurse on this node to avoid adding the positive variant return self.generic_visit(node) def visit_Expr(self, node: Expr) -> None: if isinstance(node.value, Constant) and isinstance(node.value.value, str): return self.generic_visit(node) def visit_JoinedStr(self, node): # dont recurse on JoinedStr, i.e. f strings. Constants that appear *only* # in f strings are unlikely to be helpful. return def visit_Constant(self, node): self._add_constant(node.value) self.generic_visit(node) def _constants_from_source(source: str | bytes, *, limit: bool) -> Constants: tree = ast.parse(source) visitor = ConstantVisitor(limit=limit) try: visitor.visit(tree) except TooManyConstants: # in the case of an incomplete collection, return nothing, to avoid # muddying caches etc. return Constants() return visitor.constants def _constants_file_str(constants: Constants) -> str: return str(sorted(constants, key=lambda v: (str(type(v)), v))) @lru_cache(4096) def constants_from_module(module: ModuleType, *, limit: bool = True) -> Constants: try: module_file = inspect.getsourcefile(module) # use type: ignore because we know this might error source_bytes = Path(module_file).read_bytes() # type: ignore except Exception: return Constants() if limit and len(source_bytes) > 512 * 1024: # Skip files over 512kb. For reference, the largest source file # in Hypothesis is strategies/_internal/core.py at 107kb at time # of writing. return Constants() source_hash = hashlib.sha1(source_bytes).hexdigest()[:16] # separate cache files for each limit param. see discussion in pull/4398 cache_p = storage_directory("constants") / ( source_hash + ("" if limit else "_nolimit") ) try: return _constants_from_source(cache_p.read_bytes(), limit=limit) except Exception: # if the cached location doesn't exist, or it does exist but there was # a problem reading it, fall back to standard computation of the constants pass try: constants = _constants_from_source(source_bytes, limit=limit) except Exception: # A bunch of things can go wrong here. # * ast.parse may fail on the source code # * NodeVisitor may hit a RecursionError (see many related issues on # e.g. libcst https://github.com/Instagram/LibCST/issues?q=recursion), # or a MemoryError (`"[1, " * 200 + "]" * 200`) return Constants() try: cache_p.parent.mkdir(parents=True, exist_ok=True) cache_p.write_text( f"# file: {module_file}\n# hypothesis_version: {hypothesis.__version__}\n\n" # somewhat arbitrary sort order. The cache file doesn't *have* to be # stable... but it is aesthetically pleasing, and means we could rely # on it in the future! + _constants_file_str(constants), encoding="utf-8", ) except Exception: # pragma: no cover pass return constants @lru_cache(4096) def is_local_module_file(path: str) -> bool: from hypothesis.internal.scrutineer import ModuleLocation return ( # Skip expensive path lookup for stdlib modules. # This will cause false negatives if a user names their module the # same as a stdlib module. path not in sys.stdlib_module_names # A path containing site-packages is extremely likely to be # ModuleLocation.SITE_PACKAGES. Skip the expensive path lookup here. and "/site-packages/" not in path and ModuleLocation.from_path(path) is ModuleLocation.LOCAL # normally, hypothesis is a third-party library and is not returned # by local_modules. However, if it is installed as an editable package # with pip install -e, then we will pick up on it. Just hardcode an # ignore here. and not is_hypothesis_file(path) # avoid collecting constants from test files and not ( "test" in (p := Path(path)).parts or "tests" in p.parts or p.stem.startswith("test_") or p.stem.endswith("_test") ) ) ================================================ FILE: hypothesis-python/src/hypothesis/internal/coverage.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import json import os import sys from collections.abc import Callable from contextlib import contextmanager from typing import TypeVar from hypothesis.internal.reflection import proxies """ This module implements a custom coverage system that records conditions and then validates that every condition has been seen to be both True and False during the execution of our tests. The only thing we use it for at present is our argument validation functions, where we assert that every validation function has been seen to both pass and fail in the course of testing. When not running with a magic environment variable set, this module disables itself and has essentially no overhead. """ Func = TypeVar("Func", bound=Callable) pretty_file_name_cache: dict[str, str] = {} def pretty_file_name(f): try: return pretty_file_name_cache[f] except KeyError: pass parts = f.split(os.path.sep) if "hypothesis" in parts: # pragma: no branch parts = parts[-parts[::-1].index("hypothesis") :] result = os.path.sep.join(parts) pretty_file_name_cache[f] = result return result IN_COVERAGE_TESTS = os.getenv("HYPOTHESIS_INTERNAL_COVERAGE") == "true" description_stack = [] if IN_COVERAGE_TESTS: # By this point, "branch-check" should have already been deleted by the # tox config. We can't delete it here because of #1718. written: set[tuple[str, bool]] = set() def record_branch(name, value): key = (name, value) if key in written: return written.add(key) with open(f"branch-check-{os.getpid()}", mode="a", encoding="utf-8") as log: log.write(json.dumps({"name": name, "value": value}) + "\n") @contextmanager def check_block(name, depth): # We add an extra two callers to the stack: One for the contextmanager # function, one for our actual caller, so we want to go two extra # stack frames up. caller = sys._getframe(depth + 2) fname = pretty_file_name(caller.f_code.co_filename) local_description = f"{name} at {fname}:{caller.f_lineno}" try: description_stack.append(local_description) description = " in ".join(reversed(description_stack)) + " passed" yield record_branch(description, True) except BaseException: record_branch(description, False) raise finally: description_stack.pop() @contextmanager def check(name): with check_block(name, 2): yield def check_function(f: Func) -> Func: @proxies(f) def accept(*args, **kwargs): # depth of 2 because of the proxy function calling us. with check_block(f.__name__, 2): return f(*args, **kwargs) return accept else: # pragma: no cover def check_function(f: Func) -> Func: return f @contextmanager def check(name): yield ================================================ FILE: hypothesis-python/src/hypothesis/internal/detection.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from types import MethodType def is_hypothesis_test(f: object) -> bool: """ Returns ``True`` if ``f`` represents a test function that has been defined with Hypothesis. This is true for: * Functions decorated with |@given| * The ``runTest`` method of stateful tests For example: .. code-block:: python @given(st.integers()) def f(n): ... class MyStateMachine(RuleBasedStateMachine): ... assert is_hypothesis_test(f) assert is_hypothesis_test(MyStateMachine.TestCase().runTest) .. seealso:: See also the :doc:`Detect Hypothesis tests ` how-to. """ if isinstance(f, MethodType): return is_hypothesis_test(f.__func__) return getattr(f, "is_hypothesis_test", False) ================================================ FILE: hypothesis-python/src/hypothesis/internal/entropy.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import contextlib import gc import random import sys import warnings from collections.abc import Callable, Generator, Hashable from itertools import count from random import Random from typing import TYPE_CHECKING, Any from weakref import WeakValueDictionary import hypothesis.core from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.internal.compat import FREE_THREADED_CPYTHON, GRAALPY, PYPY if TYPE_CHECKING: from typing import Protocol # we can't use this at runtime until from_type supports # protocols -- breaks ghostwriter tests class RandomLike(Protocol): def seed(self, *args: Any, **kwargs: Any) -> Any: ... def getstate(self, *args: Any, **kwargs: Any) -> Any: ... def setstate(self, *args: Any, **kwargs: Any) -> Any: ... else: # pragma: no cover RandomLike = random.Random _RKEY = count() _global_random_rkey = next(_RKEY) # This is effectively a WeakSet, which allows us to associate the saved states # with their respective Random instances even as new ones are registered and old # ones go out of scope and get garbage collected. Keys are ascending integers. RANDOMS_TO_MANAGE: WeakValueDictionary[int, RandomLike] = WeakValueDictionary( {_global_random_rkey: random} ) class NumpyRandomWrapper: def __init__(self) -> None: assert "numpy" in sys.modules # This class provides a shim that matches the numpy to stdlib random, # and lets us avoid importing Numpy until it's already in use. import numpy.random self.seed = numpy.random.seed self.getstate = numpy.random.get_state self.setstate = numpy.random.set_state NP_RANDOM: RandomLike | None = None if not (PYPY or GRAALPY): def _get_platform_base_refcount(r: Any) -> int: return sys.getrefcount(r) # Determine the number of refcounts created by function scope for # the given platform / version of Python. _PLATFORM_REF_COUNT = _get_platform_base_refcount(object()) else: # pragma: no cover # PYPY and GRAALPY don't have `sys.getrefcount` _PLATFORM_REF_COUNT = -1 def register_random(r: RandomLike) -> None: """Register (a weakref to) the given Random-like instance for management by Hypothesis. You can pass instances of structural subtypes of ``random.Random`` (i.e., objects with seed, getstate, and setstate methods) to ``register_random(r)`` to have their states seeded and restored in the same way as the global PRNGs from the ``random`` and ``numpy.random`` modules. All global PRNGs, from e.g. simulation or scheduling frameworks, should be registered to prevent flaky tests. Hypothesis will ensure that the PRNG state is consistent for all test runs, always seeding them to zero and restoring the previous state after the test, or, reproducibly varied if you choose to use the :func:`~hypothesis.strategies.random_module` strategy. ``register_random`` only makes `weakrefs `_ to ``r``, thus ``r`` will only be managed by Hypothesis as long as it has active references elsewhere at runtime. The pattern ``register_random(MyRandom())`` will raise a ``ReferenceError`` to help protect users from this issue. This check does not occur for the PyPy interpreter. See the following example for an illustration of this issue .. code-block:: python def my_BROKEN_hook(): r = MyRandomLike() # `r` will be garbage collected after the hook resolved # and Hypothesis will 'forget' that it was registered register_random(r) # Hypothesis will emit a warning rng = MyRandomLike() def my_WORKING_hook(): register_random(rng) """ if not (hasattr(r, "seed") and hasattr(r, "getstate") and hasattr(r, "setstate")): raise InvalidArgument(f"{r=} does not have all the required methods") if r in [ random for ref in RANDOMS_TO_MANAGE.data.copy().values() # type: ignore if (random := ref()) is not None ]: return if not (PYPY or GRAALPY): # pragma: no branch # PYPY and GRAALPY do not have `sys.getrefcount`. gc.collect() if not gc.get_referrers(r): if sys.getrefcount(r) <= _PLATFORM_REF_COUNT: raise ReferenceError( f"`register_random` was passed `r={r}` which will be " "garbage collected immediately after `register_random` creates a " "weakref to it. This will prevent Hypothesis from managing this " "PRNG. See the docs for `register_random` for more " "details." ) elif not FREE_THREADED_CPYTHON: # pragma: no branch # On CPython, check for the free-threaded build because # gc.get_referrers() ignores objects with immortal refcounts # and objects are immortalized in the Python 3.13 # free-threading implementation at runtime. warnings.warn( "It looks like `register_random` was passed an object that could " "be garbage collected immediately after `register_random` creates " "a weakref to it. This will prevent Hypothesis from managing this " "PRNG. See the docs for `register_random` for more details.", HypothesisWarning, stacklevel=2, ) RANDOMS_TO_MANAGE[next(_RKEY)] = r # Used to make the warning issued by `deprecate_random_in_strategy` thread-safe, # as well as to avoid warning on uses of st.randoms(). # Store just the hash to reduce memory consumption. This is an underapproximation # of membership (distinct items might have the same hash), which is fine for the # warning, as it results in missed alarms, not false alarms. _known_random_state_hashes: set[Any] = set() def get_seeder_and_restorer( seed: Hashable = 0, ) -> tuple[Callable[[], None], Callable[[], None]]: """Return a pair of functions which respectively seed all and restore the state of all registered PRNGs. This is used by the core engine via `deterministic_PRNG`, and by users via `register_random`. We support registration of additional random.Random instances (or other objects with seed, getstate, and setstate methods) to force determinism on simulation or scheduling frameworks which avoid using the global random state. See e.g. #1709. """ assert isinstance(seed, int) assert 0 <= seed < 2**32 states: dict[int, object] = {} if "numpy" in sys.modules: global NP_RANDOM if NP_RANDOM is None: # Protect this from garbage-collection by adding it to global scope NP_RANDOM = RANDOMS_TO_MANAGE[next(_RKEY)] = NumpyRandomWrapper() def seed_all() -> None: assert not states # access .data.copy().items() instead of .items() to avoid a "dictionary # changed size during iteration" error under multithreading. # # I initially expected this to be fixed by # https://github.com/python/cpython/commit/96d37dbcd23e65a7a57819aeced9034296ef747e, # but I believe that is addressing the size change from weakrefs expiring # during gc, not from the user adding new elements to the dict. # # Since we're accessing .data, we have to manually handle checking for # expired ref instances during iteration. Normally WeakValueDictionary # handles this for us. # # This command reproduces at time of writing: # pytest hypothesis-python/tests/ -k test_intervals_are_equivalent_to_their_lists # --parallel-threads 2 for k, ref in RANDOMS_TO_MANAGE.data.copy().items(): # type: ignore r = ref() if r is None: # ie the random instance has been gc'd continue # pragma: no cover states[k] = r.getstate() if k == _global_random_rkey: # r.seed sets the random's state. We want to add that state to # _known_random_states before calling r.seed, in case a thread # switch occurs between the two. To figure out the seed -> state # mapping, set the seed on a dummy random and add that state to # _known_random_state. # # we could use a global dummy random here, but then we'd have to # put a lock around it, and it's not clear to me if that's more # efficient than constructing a new instance each time. dummy_random = Random() dummy_random.seed(seed) _known_random_state_hashes.add(hash(dummy_random.getstate())) # we expect `assert r.getstate() == dummy_random.getstate()` to # hold here, but thread switches means it might not. r.seed(seed) def restore_all() -> None: for k, state in states.items(): r = RANDOMS_TO_MANAGE.get(k) if r is None: # i.e., has been garbage-collected continue if k == _global_random_rkey: _known_random_state_hashes.add(hash(state)) r.setstate(state) states.clear() return seed_all, restore_all @contextlib.contextmanager def deterministic_PRNG(seed: int = 0) -> Generator[None, None, None]: """Context manager that handles random.seed without polluting global state. See issue #1266 and PR #1295 for details and motivation - in short, leaving the global pseudo-random number generator (PRNG) seeded is a very bad idea in principle, and breaks all kinds of independence assumptions in practice. """ if ( hypothesis.core.threadlocal._hypothesis_global_random is None ): # pragma: no cover hypothesis.core.threadlocal._hypothesis_global_random = Random() register_random(hypothesis.core.threadlocal._hypothesis_global_random) seed_all, restore_all = get_seeder_and_restorer(seed) seed_all() try: yield finally: restore_all() # TODO it would be nice to clean up _known_random_state_hashes when no # active deterministic_PRNG contexts remain, to free memory (see similar # logic in StackframeLimiter). But it's a bit annoying to get right, and # likely not a big deal. ================================================ FILE: hypothesis-python/src/hypothesis/internal/escalation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import contextlib import os import sys import textwrap import traceback from collections.abc import Callable from dataclasses import dataclass from functools import partial from inspect import getfile, getsourcefile from pathlib import Path from types import ModuleType, TracebackType import hypothesis from hypothesis.errors import _Trimmable from hypothesis.internal.compat import BaseExceptionGroup from hypothesis.utils.dynamicvariables import DynamicVariable FILE_CACHE: dict[ModuleType, dict[str, bool]] = {} def belongs_to(package: ModuleType) -> Callable[[str], bool]: if getattr(package, "__file__", None) is None: # pragma: no cover return lambda filepath: False assert package.__file__ is not None FILE_CACHE.setdefault(package, {}) cache = FILE_CACHE[package] root = Path(package.__file__).resolve().parent def accept(filepath: str) -> bool: try: return cache[filepath] except KeyError: pass try: Path(filepath).resolve().relative_to(root) result = True except Exception: result = False cache[filepath] = result return result accept.__name__ = f"is_{package.__name__}_file" return accept is_hypothesis_file = belongs_to(hypothesis) def get_trimmed_traceback( exception: BaseException | None = None, ) -> TracebackType | None: """Return the current traceback, minus any frames added by Hypothesis.""" if exception is None: _, exception, tb = sys.exc_info() else: tb = exception.__traceback__ # Avoid trimming the traceback if we're in verbose mode, or the error # was raised inside Hypothesis. Additionally, the environment variable # HYPOTHESIS_NO_TRACEBACK_TRIM is respected if nonempty, because verbose # mode is prohibitively slow when debugging strategy recursion errors. assert hypothesis.settings.default is not None if ( tb is None or os.environ.get("HYPOTHESIS_NO_TRACEBACK_TRIM") or hypothesis.settings.default.verbosity >= hypothesis.Verbosity.debug or ( is_hypothesis_file(traceback.extract_tb(tb)[-1][0]) and not isinstance(exception, _Trimmable) ) ): return tb while tb.tb_next is not None and ( # If the frame is from one of our files, it's been added by Hypothesis. is_hypothesis_file(getsourcefile(tb.tb_frame) or getfile(tb.tb_frame)) # But our `@proxies` decorator overrides the source location, # so we check for an attribute it injects into the frame too. or tb.tb_frame.f_globals.get("__hypothesistracebackhide__") is True ): tb = tb.tb_next return tb @dataclass(slots=True, frozen=True) class InterestingOrigin: # The `interesting_origin` is how Hypothesis distinguishes between multiple # failures, for reporting and also to replay from the example database (even # if report_multiple_bugs=False). We traditionally use the exception type and # location, but have extracted this logic in order to see through `except ...:` # blocks and understand the __cause__ (`raise x from y`) or __context__ that # first raised an exception as well as PEP-654 exception groups. exc_type: type[BaseException] filename: str | None lineno: int | None context: "InterestingOrigin | tuple[()]" group_elems: "tuple[InterestingOrigin, ...]" def __str__(self) -> str: ctx = "" if self.context: ctx = textwrap.indent(f"\ncontext: {self.context}", prefix=" ") group = "" if self.group_elems: chunks = "\n ".join(str(x) for x in self.group_elems) group = textwrap.indent(f"\nchild exceptions:\n {chunks}", prefix=" ") return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}" @classmethod def from_exception( cls, exception: BaseException, /, seen: tuple[BaseException, ...] = () ) -> "InterestingOrigin": filename, lineno = None, None if tb := get_trimmed_traceback(exception): filename, lineno, *_ = traceback.extract_tb(tb)[-1] seen = (*seen, exception) make = partial(cls.from_exception, seen=seen) context: InterestingOrigin | tuple[()] = () if exception.__context__ is not None and exception.__context__ not in seen: context = make(exception.__context__) return cls( type(exception), filename, lineno, # Note that if __cause__ is set it is always equal to __context__, explicitly # to support introspection when debugging, so we can use that unconditionally. context, # We distinguish exception groups by the inner exceptions, as for __context__ ( tuple(make(exc) for exc in exception.exceptions if exc not in seen) if isinstance(exception, BaseExceptionGroup) else () ), ) current_pytest_item = DynamicVariable(None) def _get_exceptioninfo(): # ExceptionInfo was moved to the top-level namespace in Pytest 7.0 if "pytest" in sys.modules: with contextlib.suppress(Exception): # From Pytest 7, __init__ warns on direct calls. return sys.modules["pytest"].ExceptionInfo.from_exc_info if "_pytest._code" in sys.modules: # old versions only with contextlib.suppress(Exception): return sys.modules["_pytest._code"].ExceptionInfo return None # pragma: no cover # coverage tests always use pytest def format_exception(err, tb): # Try using Pytest to match the currently configured traceback style ExceptionInfo = _get_exceptioninfo() if current_pytest_item.value is not None and ExceptionInfo is not None: item = current_pytest_item.value return str(item.repr_failure(ExceptionInfo((type(err), err, tb)))) + "\n" # Or use better_exceptions, if that's installed and enabled if "better_exceptions" in sys.modules: better_exceptions = sys.modules["better_exceptions"] if sys.excepthook is better_exceptions.excepthook: return "".join(better_exceptions.format_exception(type(err), err, tb)) # If all else fails, use the standard-library formatting tools return "".join(traceback.format_exception(type(err), err, tb)) ================================================ FILE: hypothesis-python/src/hypothesis/internal/filtering.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Tools for understanding predicates, to satisfy them by construction. For example:: integers().filter(lambda x: x >= 0) -> integers(min_value=0) This is intractable in general, but reasonably easy for simple cases involving numeric bounds, strings with length or regex constraints, and collection lengths - and those are precisely the most common cases. When they arise in e.g. Pandas dataframes, it's also pretty painful to do the constructive version by hand in a library; so we prefer to share all the implementation effort here. See https://github.com/HypothesisWorks/hypothesis/issues/2701 for details. """ import ast import inspect import math import operator import sys from collections.abc import Callable, Collection from decimal import Decimal from fractions import Fraction from functools import partial from typing import Any, NamedTuple, TypeVar from hypothesis.internal.compat import ceil, floor from hypothesis.internal.floats import next_down, next_up from hypothesis.internal.lambda_sources import lambda_description from hypothesis.internal.reflection import get_pretty_function_description if sys.version_info[:2] >= (3, 14): from functools import Placeholder else: # pragma: no cover Placeholder = object() Ex = TypeVar("Ex") Predicate = Callable[[Ex], bool] class ConstructivePredicate(NamedTuple): """Return constraints to the appropriate strategy, and the predicate if needed. For example:: integers().filter(lambda x: x >= 0) -> {"min_value": 0"}, None integers().filter(lambda x: x >= 0 and x % 7) -> {"min_value": 0}, lambda x: x % 7 At least in principle - for now we usually return the predicate unchanged if needed. We have a separate get-predicate frontend for each "group" of strategies; e.g. for each numeric type, for strings, for bytes, for collection sizes, etc. """ constraints: dict[str, Any] predicate: Predicate | None @classmethod def unchanged(cls, predicate: Predicate) -> "ConstructivePredicate": return cls({}, predicate) def __repr__(self) -> str: fn = get_pretty_function_description(self.predicate) return f"{self.__class__.__name__}(constraints={self.constraints!r}, predicate={fn})" ARG = object() def convert(node: ast.AST, argname: str) -> object: if isinstance(node, ast.Name): if node.id != argname: raise ValueError("Non-local variable") return ARG if isinstance(node, ast.Call): if ( isinstance(node.func, ast.Name) and node.func.id == "len" and len(node.args) == 1 ): # error unless comparison is to the len *of the lambda arg* return convert(node.args[0], argname) return ast.literal_eval(node) def comp_to_constraints(x: ast.AST, op: ast.AST, y: ast.AST, *, argname: str) -> dict: a = convert(x, argname) b = convert(y, argname) num = (int, float) if not (a is ARG and isinstance(b, num)) and not (isinstance(a, num) and b is ARG): # It would be possible to work out if comparisons between two literals # are always true or false, but it's too rare to be worth the complexity. # (and we can't even do `arg == arg`, because what if it's NaN?) raise ValueError("Can't analyse this comparison") of_len = {"len": True} if isinstance(x, ast.Call) or isinstance(y, ast.Call) else {} if isinstance(op, ast.Lt): if a is ARG: return {"max_value": b, "exclude_max": True, **of_len} return {"min_value": a, "exclude_min": True, **of_len} elif isinstance(op, ast.LtE): if a is ARG: return {"max_value": b, **of_len} return {"min_value": a, **of_len} elif isinstance(op, ast.Eq): if a is ARG: return {"min_value": b, "max_value": b, **of_len} return {"min_value": a, "max_value": a, **of_len} elif isinstance(op, ast.GtE): if a is ARG: return {"min_value": b, **of_len} return {"max_value": a, **of_len} elif isinstance(op, ast.Gt): if a is ARG: return {"min_value": b, "exclude_min": True, **of_len} return {"max_value": a, "exclude_max": True, **of_len} raise ValueError("Unhandled comparison operator") # e.g. ast.Ne def merge_preds(*con_predicates: ConstructivePredicate) -> ConstructivePredicate: # This function is just kinda messy. Unfortunately the neatest way # to do this is just to roll out each case and handle them in turn. base = { "min_value": -math.inf, "max_value": math.inf, "exclude_min": False, "exclude_max": False, } predicate = None for kw, p in con_predicates: assert ( not p or not predicate or p is predicate ), "Can't merge two partially-constructive preds" predicate = p or predicate if "min_value" in kw: if kw["min_value"] > base["min_value"]: base["exclude_min"] = kw.get("exclude_min", False) base["min_value"] = kw["min_value"] elif kw["min_value"] == base["min_value"]: base["exclude_min"] |= kw.get("exclude_min", False) if "max_value" in kw: if kw["max_value"] < base["max_value"]: base["exclude_max"] = kw.get("exclude_max", False) base["max_value"] = kw["max_value"] elif kw["max_value"] == base["max_value"]: base["exclude_max"] |= kw.get("exclude_max", False) has_len = {"len" in kw for kw, _ in con_predicates if kw} assert len(has_len) <= 1, "can't mix numeric with length constraints" if has_len == {True}: base["len"] = True if not base["exclude_min"]: del base["exclude_min"] if base["min_value"] == -math.inf: del base["min_value"] if not base["exclude_max"]: del base["exclude_max"] if base["max_value"] == math.inf: del base["max_value"] return ConstructivePredicate(base, predicate) def numeric_bounds_from_ast( tree: ast.AST, argname: str, fallback: ConstructivePredicate ) -> ConstructivePredicate: """Take an AST; return a ConstructivePredicate. >>> lambda x: x >= 0 {"min_value": 0}, None >>> lambda x: x < 10 {"max_value": 10, "exclude_max": True}, None >>> lambda x: len(x) >= 5 {"min_value": 5, "len": True}, None >>> lambda x: x >= y {}, lambda x: x >= y See also https://greentreesnakes.readthedocs.io/en/latest/ """ if isinstance(tree, ast.Compare): ops = tree.ops vals = tree.comparators comparisons = [(tree.left, ops[0], vals[0])] for i, (op, val) in enumerate(zip(ops[1:], vals[1:], strict=True), start=1): comparisons.append((vals[i - 1], op, val)) bounds = [] for comp in comparisons: try: constraints = comp_to_constraints(*comp, argname=argname) # Because `len` could be redefined in the enclosing scope, we *always* # have to apply the condition as a filter, in addition to rewriting. pred = fallback.predicate if "len" in constraints else None bounds.append(ConstructivePredicate(constraints, pred)) except ValueError: bounds.append(fallback) return merge_preds(*bounds) if isinstance(tree, ast.BoolOp) and isinstance(tree.op, ast.And): return merge_preds( *(numeric_bounds_from_ast(node, argname, fallback) for node in tree.values) ) return fallback def get_numeric_predicate_bounds(predicate: Predicate) -> ConstructivePredicate: """Shared logic for understanding numeric bounds. We then specialise this in the other functions below, to ensure that e.g. all the values are representable in the types that we're planning to generate so that the strategy validation doesn't complain. """ unchanged = ConstructivePredicate.unchanged(predicate) if ( isinstance(predicate, partial) and not predicate.keywords and ( len(predicate.args) == 1 or (predicate.args[0] is Placeholder and len(predicate.args) == 2) ) ): if len(predicate.args) == 1: arg = predicate.args[0] func = predicate.func else: # pragma: no cover # Python 3.14+ only assert predicate.args[0] is Placeholder arg = predicate.args[1] func = { # reverses the table below; eq is unchanged operator.lt: operator.gt, operator.le: operator.ge, operator.ge: operator.le, operator.gt: operator.lt, }.get(predicate.func, predicate.func) assert func not in (min_len, max_len) # sanity-check; these are private if ( (isinstance(arg, Decimal) and Decimal.is_snan(arg)) or not isinstance(arg, (int, float, Fraction, Decimal)) or math.isnan(arg) ): return unchanged options = { # We're talking about op(arg, x) - the reverse of our usual intuition! operator.lt: {"min_value": arg, "exclude_min": True}, # lambda x: arg < x operator.le: {"min_value": arg}, # lambda x: arg <= x operator.eq: {"min_value": arg, "max_value": arg}, # lambda x: arg == x operator.ge: {"max_value": arg}, # lambda x: arg >= x operator.gt: {"max_value": arg, "exclude_max": True}, # lambda x: arg > x # Special-case our default predicates for length bounds min_len: {"min_value": arg, "len": True}, max_len: {"max_value": arg, "len": True}, } if func in options: return ConstructivePredicate(options[func], None) # This section is a little complicated, but stepping through with comments should # help to clarify it. We start by finding the source code for our predicate and # parsing it to an abstract syntax tree; if this fails for any reason we bail out # and fall back to standard rejection sampling (a running theme). try: if predicate.__name__ == "": source = lambda_description(predicate) else: source = inspect.getsource(predicate) tree: ast.AST = ast.parse(source) except Exception: return unchanged # Dig down to the relevant subtree - our tree is probably a Module containing # either a FunctionDef, or an Expr which in turn contains a lambda definition. while isinstance(tree, ast.Module) and len(tree.body) == 1: tree = tree.body[0] while isinstance(tree, ast.Expr): tree = tree.value if isinstance(tree, ast.Lambda) and len(tree.args.args) == 1: return numeric_bounds_from_ast(tree.body, tree.args.args[0].arg, unchanged) elif isinstance(tree, ast.FunctionDef) and len(tree.args.args) == 1: if len(tree.body) != 1 or not isinstance(tree.body[0], ast.Return): # If the body of the function is anything but `return `, # i.e. as simple as a lambda, we can't process it (yet). return unchanged argname = tree.args.args[0].arg body = tree.body[0].value assert isinstance(body, ast.AST) return numeric_bounds_from_ast(body, argname, unchanged) return unchanged def get_integer_predicate_bounds(predicate: Predicate) -> ConstructivePredicate: constraints, predicate = get_numeric_predicate_bounds(predicate) if "min_value" in constraints: if constraints["min_value"] == -math.inf: del constraints["min_value"] elif math.isinf(constraints["min_value"]): return ConstructivePredicate({"min_value": 1, "max_value": -1}, None) elif constraints["min_value"] != int(constraints["min_value"]): constraints["min_value"] = ceil(constraints["min_value"]) elif constraints.get("exclude_min", False): constraints["min_value"] = int(constraints["min_value"]) + 1 if "max_value" in constraints: if constraints["max_value"] == math.inf: del constraints["max_value"] elif math.isinf(constraints["max_value"]): return ConstructivePredicate({"min_value": 1, "max_value": -1}, None) elif constraints["max_value"] != int(constraints["max_value"]): constraints["max_value"] = floor(constraints["max_value"]) elif constraints.get("exclude_max", False): constraints["max_value"] = int(constraints["max_value"]) - 1 kw_categories = {"min_value", "max_value", "len"} constraints = {k: v for k, v in constraints.items() if k in kw_categories} return ConstructivePredicate(constraints, predicate) def get_float_predicate_bounds(predicate: Predicate) -> ConstructivePredicate: constraints, predicate = get_numeric_predicate_bounds(predicate) if "min_value" in constraints: min_value = constraints["min_value"] constraints["min_value"] = float(constraints["min_value"]) if min_value < constraints["min_value"] or ( min_value == constraints["min_value"] and constraints.get("exclude_min", False) ): constraints["min_value"] = next_up(constraints["min_value"]) if "max_value" in constraints: max_value = constraints["max_value"] constraints["max_value"] = float(constraints["max_value"]) if max_value > constraints["max_value"] or ( max_value == constraints["max_value"] and constraints.get("exclude_max", False) ): constraints["max_value"] = next_down(constraints["max_value"]) constraints = { k: v for k, v in constraints.items() if k in {"min_value", "max_value"} } return ConstructivePredicate(constraints, predicate) def max_len(size: int, element: Collection[object]) -> bool: return len(element) <= size def min_len(size: int, element: Collection[object]) -> bool: return size <= len(element) ================================================ FILE: hypothesis-python/src/hypothesis/internal/floats.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import struct from collections.abc import Callable from sys import float_info from typing import Literal, SupportsFloat, TypeAlias SignedIntFormat: TypeAlias = Literal["!h", "!i", "!q"] UnsignedIntFormat: TypeAlias = Literal["!H", "!I", "!Q"] IntFormat: TypeAlias = SignedIntFormat | UnsignedIntFormat FloatFormat: TypeAlias = Literal["!e", "!f", "!d"] Width: TypeAlias = Literal[16, 32, 64] # Format codes for (int, float) sized types, used for byte-wise casts. # See https://docs.python.org/3/library/struct.html#format-characters STRUCT_FORMATS: dict[int, tuple[UnsignedIntFormat, FloatFormat]] = { 16: ("!H", "!e"), 32: ("!I", "!f"), 64: ("!Q", "!d"), } TO_SIGNED_FORMAT: dict[UnsignedIntFormat, SignedIntFormat] = { "!H": "!h", "!I": "!i", "!Q": "!q", } def reinterpret_bits(x: float | int, from_: str, to: str) -> float | int: x = struct.unpack(to, struct.pack(from_, x))[0] assert isinstance(x, (float, int)) return x def float_of(x: SupportsFloat, width: Width) -> float: assert width in (16, 32, 64) if width == 64: return float(x) elif width == 32: return reinterpret_bits(float(x), "!f", "!f") else: return reinterpret_bits(float(x), "!e", "!e") def is_negative(x: SupportsFloat) -> bool: try: return math.copysign(1.0, x) < 0 except TypeError: raise TypeError( f"Expected float but got {x!r} of type {type(x).__name__}" ) from None def count_between_floats(x: float, y: float, width: int = 64) -> int: assert x <= y if is_negative(x): if is_negative(y): return float_to_int(x, width) - float_to_int(y, width) + 1 else: return count_between_floats(x, -0.0, width) + count_between_floats( 0.0, y, width ) else: assert not is_negative(y) return float_to_int(y, width) - float_to_int(x, width) + 1 def float_to_int(value: float, width: int = 64) -> int: fmt_int, fmt_flt = STRUCT_FORMATS[width] x = reinterpret_bits(value, fmt_flt, fmt_int) assert isinstance(x, int) return x def int_to_float(value: int, width: int = 64) -> float: fmt_int, fmt_flt = STRUCT_FORMATS[width] return reinterpret_bits(value, fmt_int, fmt_flt) def next_up(value: float, width: int = 64) -> float: """Return the first float larger than finite `val` - IEEE 754's `nextUp`. From https://stackoverflow.com/a/10426033, with thanks to Mark Dickinson. """ assert isinstance(value, float), f"{value!r} of type {type(value)}" if math.isnan(value) or (math.isinf(value) and value > 0): return value if value == 0.0 and is_negative(value): return 0.0 fmt_int, fmt_flt = STRUCT_FORMATS[width] # Note: n is signed; float_to_int returns unsigned fmt_int_signed = TO_SIGNED_FORMAT[fmt_int] n = reinterpret_bits(value, fmt_flt, fmt_int_signed) if n >= 0: n += 1 else: n -= 1 return reinterpret_bits(n, fmt_int_signed, fmt_flt) def next_down(value: float, width: int = 64) -> float: return -next_up(-value, width) def next_down_normal(value: float, width: int, *, allow_subnormal: bool) -> float: value = next_down(value, width) if (not allow_subnormal) and 0 < abs(value) < width_smallest_normals[width]: return 0.0 if value > 0 else -width_smallest_normals[width] return value def next_up_normal(value: float, width: int, *, allow_subnormal: bool) -> float: return -next_down_normal(-value, width, allow_subnormal=allow_subnormal) # Smallest positive non-zero numbers that is fully representable by an # IEEE-754 float, calculated with the width's associated minimum exponent. # Values from https://en.wikipedia.org/wiki/IEEE_754#Basic_and_interchange_formats width_smallest_normals: dict[int, float] = { 16: 2 ** -(2 ** (5 - 1) - 2), 32: 2 ** -(2 ** (8 - 1) - 2), 64: 2 ** -(2 ** (11 - 1) - 2), } assert width_smallest_normals[64] == float_info.min mantissa_mask = (1 << 52) - 1 def make_float_clamper( min_value: float, max_value: float, *, allow_nan: bool, smallest_nonzero_magnitude: float, ) -> Callable[[float], float]: """ Return a function that clamps positive floats into the given bounds. """ from hypothesis.internal.conjecture.choice import choice_permitted assert sign_aware_lte(min_value, max_value) range_size = min(max_value - min_value, float_info.max) def float_clamper(f: float) -> float: if choice_permitted( f, { "min_value": min_value, "max_value": max_value, "allow_nan": allow_nan, "smallest_nonzero_magnitude": smallest_nonzero_magnitude, }, ): return f # Outside bounds; pick a new value, sampled from the allowed range, # using the mantissa bits. mant = float_to_int(abs(f)) & mantissa_mask f = min_value + range_size * (mant / mantissa_mask) # if we resampled into the space disallowed by smallest_nonzero_magnitude, # default to smallest_nonzero_magnitude. if 0 < abs(f) < smallest_nonzero_magnitude: f = smallest_nonzero_magnitude # we must have either -smallest_nonzero_magnitude <= min_value or # smallest_nonzero_magnitude >= max_value, or no values would be # possible. If smallest_nonzero_magnitude is not valid (because it's # larger than max_value), then -smallest_nonzero_magnitude must be valid. if smallest_nonzero_magnitude > max_value: f *= -1 # Re-enforce the bounds (just in case of floating point arithmetic error) return clamp(min_value, f, max_value) return float_clamper def sign_aware_lte(x: float | int, y: float | int) -> bool: """Less-than-or-equals, but strictly orders -0.0 and 0.0""" if x == 0.0 == y: return math.copysign(1.0, x) <= math.copysign(1.0, y) else: return x <= y def clamp(lower: float | int, value: float | int, upper: float | int) -> float | int: """Given a value and lower/upper bounds, 'clamp' the value so that it satisfies lower <= value <= upper. NaN is mapped to lower.""" # this seems pointless (and is for integers), but handles the -0.0/0.0 case. if not sign_aware_lte(lower, value): return lower if not sign_aware_lte(value, upper): return upper return value SMALLEST_SUBNORMAL = next_up(0.0) SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa MAX_PRECISE_INTEGER = 2**53 assert math.isnan(SIGNALING_NAN) assert math.copysign(1, SIGNALING_NAN) == 1 ================================================ FILE: hypothesis-python/src/hypothesis/internal/healthcheck.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.errors import FailedHealthCheck def fail_health_check(settings, message, label): # Tell pytest to omit the body of this function from tracebacks # https://docs.pytest.org/en/latest/example/simple.html#writing-well-integrated-assertion-helpers __tracebackhide__ = True if label in settings.suppress_health_check: return raise FailedHealthCheck(message) ================================================ FILE: hypothesis-python/src/hypothesis/internal/intervalsets.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Iterable, Sequence from typing import TYPE_CHECKING, TypeAlias, cast, final if TYPE_CHECKING: from typing_extensions import Self IntervalsT: TypeAlias = tuple[tuple[int, int], ...] # @final makes mypy happy with the Self return annotations. We otherwise run # afoul of: # > You should not use Self as the return annotation if the method is not # > guaranteed to return an instance of a subclass when the class is subclassed # > https://docs.python.org/3/library/typing.html#typing.Self @final class IntervalSet: """ A compact and efficient representation of a set of ``(a, b)`` intervals. Can be treated like a set of integers, in that ``n in intervals`` will return ``True`` if ``n`` is contained in any of the ``(a, b)`` intervals, and ``False`` otherwise. """ @classmethod def from_string(cls, s: str) -> "Self": """Return a tuple of intervals, covering the codepoints of characters in `s`. >>> IntervalSet.from_string('abcdef0123456789') ((48, 57), (97, 102)) """ x = cls([(ord(c), ord(c)) for c in sorted(s)]) return x.union(x) def __init__(self, intervals: Iterable[Sequence[int]] = ()) -> None: self.intervals: IntervalsT = cast( IntervalsT, tuple(tuple(v) for v in intervals) ) # cast above is validated by this length assertion. check here instead of # before to not exhaust generators before we create intervals from it assert all(len(v) == 2 for v in self.intervals) self.offsets: list[int] = [0] for u, v in self.intervals: self.offsets.append(self.offsets[-1] + v - u + 1) self.size = self.offsets.pop() self._idx_of_zero = self.index_above(ord("0")) self._idx_of_Z = min(self.index_above(ord("Z")), len(self) - 1) def __len__(self) -> int: return self.size def __iter__(self) -> Iterable[int]: for u, v in self.intervals: yield from range(u, v + 1) def __getitem__(self, i: int) -> int: if i < 0: i = self.size + i if i < 0 or i >= self.size: raise IndexError(f"Invalid index {i} for [0, {self.size})") # Want j = maximal such that offsets[j] <= i j = len(self.intervals) - 1 if self.offsets[j] > i: hi = j lo = 0 # Invariant: offsets[lo] <= i < offsets[hi] while lo + 1 < hi: mid = (lo + hi) // 2 if self.offsets[mid] <= i: lo = mid else: hi = mid j = lo t = i - self.offsets[j] u, v = self.intervals[j] r = u + t assert r <= v return r def __contains__(self, elem: str | int) -> bool: if isinstance(elem, str): elem = ord(elem) assert 0 <= elem <= 0x10FFFF return any(start <= elem <= end for start, end in self.intervals) def __repr__(self) -> str: return f"IntervalSet({self.intervals!r})" def index(self, value: int) -> int: for offset, (u, v) in zip(self.offsets, self.intervals, strict=True): if u == value: return offset elif u > value: raise ValueError(f"{value} is not in list") if value <= v: return offset + (value - u) raise ValueError(f"{value} is not in list") def index_above(self, value: int) -> int: for offset, (u, v) in zip(self.offsets, self.intervals, strict=True): if u >= value: return offset if value <= v: return offset + (value - u) return self.size def __or__(self, other: "Self") -> "Self": return self.union(other) def __sub__(self, other: "Self") -> "Self": return self.difference(other) def __and__(self, other: "Self") -> "Self": return self.intersection(other) def __eq__(self, other: object) -> bool: return isinstance(other, IntervalSet) and (other.intervals == self.intervals) def __hash__(self) -> int: return hash(self.intervals) def union(self, other: "Self") -> "Self": """Merge two sequences of intervals into a single tuple of intervals. Any integer bounded by `x` or `y` is also bounded by the result. >>> union([(3, 10)], [(1, 2), (5, 17)]) ((1, 17),) """ assert isinstance(other, type(self)) x = self.intervals y = other.intervals if not x: return IntervalSet(y) if not y: return IntervalSet(x) intervals = sorted(x + y, reverse=True) result = [intervals.pop()] while intervals: # 1. intervals is in descending order # 2. pop() takes from the RHS. # 3. (a, b) was popped 1st, then (u, v) was popped 2nd # 4. Therefore: a <= u # 5. We assume that u <= v and a <= b # 6. So we need to handle 2 cases of overlap, and one disjoint case # | u--v | u----v | u--v | # | a----b | a--b | a--b | u, v = intervals.pop() a, b = result[-1] if u <= b + 1: # Overlap cases result[-1] = (a, max(v, b)) else: # Disjoint case result.append((u, v)) return IntervalSet(result) def difference(self, other: "Self") -> "Self": """Set difference for lists of intervals. That is, returns a list of intervals that bounds all values bounded by x that are not also bounded by y. x and y are expected to be in sorted order. For example difference([(1, 10)], [(2, 3), (9, 15)]) would return [(1, 1), (4, 8)], removing the values 2, 3, 9 and 10 from the interval. """ assert isinstance(other, type(self)) x = self.intervals y = other.intervals if not y: return IntervalSet(x) x = list(map(list, x)) i = 0 j = 0 result: list[Iterable[int]] = [] while i < len(x) and j < len(y): # Iterate in parallel over x and y. j stays pointing at the smallest # interval in the left hand side that could still overlap with some # element of x at index >= i. # Similarly, i is not incremented until we know that it does not # overlap with any element of y at index >= j. xl, xr = x[i] assert xl <= xr yl, yr = y[j] assert yl <= yr if yr < xl: # The interval at y[j] is strictly to the left of the interval at # x[i], so will not overlap with it or any later interval of x. j += 1 elif yl > xr: # The interval at y[j] is strictly to the right of the interval at # x[i], so all of x[i] goes into the result as no further intervals # in y will intersect it. result.append(x[i]) i += 1 elif yl <= xl: if yr >= xr: # x[i] is contained entirely in y[j], so we just skip over it # without adding it to the result. i += 1 else: # The beginning of x[i] is contained in y[j], so we update the # left endpoint of x[i] to remove this, and increment j as we # now have moved past it. Note that this is not added to the # result as is, as more intervals from y may intersect it so it # may need updating further. x[i][0] = yr + 1 j += 1 else: # yl > xl, so the left hand part of x[i] is not contained in y[j], # so there are some values we should add to the result. result.append((xl, yl - 1)) if yr + 1 <= xr: # If y[j] finishes before x[i] does, there may be some values # in x[i] left that should go in the result (or they may be # removed by a later interval in y), so we update x[i] to # reflect that and increment j because it no longer overlaps # with any remaining element of x. x[i][0] = yr + 1 j += 1 else: # Every element of x[i] other than the initial part we have # already added is contained in y[j], so we move to the next # interval. i += 1 # Any remaining intervals in x do not overlap with any of y, as if they did # we would not have incremented j to the end, so can be added to the result # as they are. result.extend(x[i:]) return IntervalSet(map(tuple, result)) def intersection(self, other: "Self") -> "Self": """Set intersection for lists of intervals.""" assert isinstance(other, type(self)), other intervals = [] i = j = 0 while i < len(self.intervals) and j < len(other.intervals): u, v = self.intervals[i] U, V = other.intervals[j] if u > V: j += 1 elif U > v: i += 1 else: intervals.append((max(u, U), min(v, V))) if v < V: i += 1 else: j += 1 return IntervalSet(intervals) def char_in_shrink_order(self, i: int) -> str: # We would like it so that, where possible, shrinking replaces # characters with simple ascii characters, so we rejig this # bit so that the smallest values are 0, 1, 2, ..., Z. # # Imagine that numbers are laid out as abc0yyyZ... # this rearranges them so that they are laid out as # 0yyyZcba..., which gives a better shrinking order. if i <= self._idx_of_Z: # We want to rewrite the integers [0, n] inclusive # to [zero_point, Z_point]. n = self._idx_of_Z - self._idx_of_zero if i <= n: i += self._idx_of_zero else: # We want to rewrite the integers [n + 1, Z_point] to # [zero_point, 0] (reversing the order so that codepoints below # zero_point shrink upwards). i = self._idx_of_zero - (i - n) assert i < self._idx_of_zero assert 0 <= i <= self._idx_of_Z return chr(self[i]) def index_from_char_in_shrink_order(self, c: str) -> int: """ Inverse of char_in_shrink_order. """ assert len(c) == 1 i = self.index(ord(c)) if i <= self._idx_of_Z: n = self._idx_of_Z - self._idx_of_zero # Rewrite [zero_point, Z_point] to [0, n]. if self._idx_of_zero <= i <= self._idx_of_Z: i -= self._idx_of_zero assert 0 <= i <= n # Rewrite [zero_point, 0] to [n + 1, Z_point]. else: i = self._idx_of_zero - i + n assert n + 1 <= i <= self._idx_of_Z assert 0 <= i <= self._idx_of_Z return i ================================================ FILE: hypothesis-python/src/hypothesis/internal/lambda_sources.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import ast import hashlib import inspect import linecache import sys import textwrap from collections.abc import Callable, MutableMapping from inspect import Parameter from typing import Any from weakref import WeakKeyDictionary from hypothesis.internal import reflection from hypothesis.internal.cache import LRUCache # we have several levels of caching for lambda descriptions. # * LAMBDA_DESCRIPTION_CACHE maps a lambda f to its description _lambda_description(f). # Note that _lambda_description(f) may not be identical to f as it appears in the # source code file. # * LAMBDA_DIGEST_DESCRIPTION_CACHE maps _function_key(f) to _lambda_description(f). # _function_key implements something close to "ast equality": # two syntactically identical (minus whitespace etc) lambdas appearing in # different files have the same key. Cache hits here provide a fast path which # avoids ast-parsing syntactic lambdas we've seen before. Two lambdas with the # same _function_key will not have different _lambda_descriptions - if # they do, that's a bug here. # * AST_LAMBDAS_CACHE maps source code lines to a list of the lambdas found in # that source code. A cache hit here avoids reparsing the ast. LAMBDA_DESCRIPTION_CACHE: MutableMapping[Callable, str] = WeakKeyDictionary() LAMBDA_DIGEST_DESCRIPTION_CACHE: LRUCache[tuple[Any], str] = LRUCache(max_size=1000) AST_LAMBDAS_CACHE: LRUCache[tuple[str], list[ast.Lambda]] = LRUCache(max_size=100) def extract_all_lambdas(tree): lambdas = [] class Visitor(ast.NodeVisitor): def visit_Lambda(self, node): lambdas.append(node) self.visit(node.body) Visitor().visit(tree) return lambdas def extract_all_attributes(tree): attributes = [] class Visitor(ast.NodeVisitor): def visit_Attribute(self, node): attributes.append(node) self.visit(node.value) Visitor().visit(tree) return attributes def _function_key(f, *, bounded_size=False, ignore_name=False): """Returns a digest that differentiates functions that have different sources. Either a function or a code object may be passed. If code object, default arg/kwarg values are not recoverable - this is the best we can do, and is sufficient for the use case of comparing nested lambdas. """ try: code = f.__code__ defaults_repr = repr((f.__defaults__, f.__kwdefaults__)) except AttributeError: code = f defaults_repr = () consts_repr = repr(code.co_consts) if bounded_size: # Compress repr to avoid keeping arbitrarily large strings pinned as cache # keys. We don't do this unconditionally because hashing takes time, and is # not necessary if the key is used just for comparison (and is not stored). if len(consts_repr) > 48: consts_repr = hashlib.sha384(consts_repr.encode()).digest() if len(defaults_repr) > 48: defaults_repr = hashlib.sha384(defaults_repr.encode()).digest() return ( consts_repr, defaults_repr, code.co_argcount, code.co_kwonlyargcount, code.co_code, code.co_names, code.co_varnames, code.co_freevars, ignore_name or code.co_name, ) class _op: # Opcodes, from dis.opmap. These may change between major versions. NOP = 9 LOAD_FAST = 85 LOAD_FAST_LOAD_FAST = 88 LOAD_FAST_BORROW = 86 LOAD_FAST_BORROW_LOAD_FAST_BORROW = 87 def _normalize_code(f, l): # A small selection of possible peephole code transformations, based on what # is actually seen to differ between compilations in our test suite. Each # entry contains two equivalent opcode sequences, plus a condition # function called with their respective oparg sequences, which must return # true for the transformation to be valid. Checker = Callable[[list[int], list[int]], bool] transforms: tuple[list[int], list[int], Checker | None] = [ ([_op.NOP], [], lambda a, b: True), ( [_op.LOAD_FAST, _op.LOAD_FAST], [_op.LOAD_FAST_LOAD_FAST], lambda a, b: a == [b[0] >> 4, b[0] & 15], ), ( [_op.LOAD_FAST_BORROW, _op.LOAD_FAST_BORROW], [_op.LOAD_FAST_BORROW_LOAD_FAST_BORROW], lambda a, b: a == [b[0] >> 4, b[0] & 15], ), ] # augment with converse transforms += [ ( ops_b, ops_a, condition and (lambda a, b, condition=condition: condition(b, a)), ) for ops_a, ops_b, condition in transforms ] # Normalize equivalent code. We assume that each bytecode op is 2 bytes, # which is the case since Python 3.6. Since the opcodes values may change # between version, there is a risk that a transform may not be equivalent # -- even so, the risk of a bad transform producing a false positive is # minuscule. co_code = list(l.__code__.co_code) f_code = list(f.__code__.co_code) def alternating(code, i, n): return code[i : i + 2 * n : 2] i = 2 while i < max(len(co_code), len(f_code)): # note that co_code is mutated in loop if i < min(len(co_code), len(f_code)) and f_code[i] == co_code[i]: i += 2 else: for op1, op2, condition in transforms: if ( op1 == alternating(f_code, i, len(op1)) and op2 == alternating(co_code, i, len(op2)) and condition( alternating(f_code, i + 1, len(op1)), alternating(co_code, i + 1, len(op2)), ) ): break else: # no point in continuing since the bytecodes are different anyway break # Splice in the transform and continue co_code = ( co_code[:i] + f_code[i : i + 2 * len(op1)] + co_code[i + 2 * len(op2) :] ) i += 2 * len(op1) # Normalize consts, in particular replace any lambda consts with the # corresponding const from the template function, IFF they have the same # source key. f_consts = f.__code__.co_consts l_consts = l.__code__.co_consts if len(f_consts) == len(l_consts) and any( inspect.iscode(l_const) for l_const in l_consts ): normalized_consts = [] for f_const, l_const in zip(f_consts, l_consts, strict=True): if ( inspect.iscode(l_const) and inspect.iscode(f_const) and _function_key(f_const) == _function_key(l_const) ): # If the lambdas are compiled from the same source, make them be the # same object so that the toplevel lambdas end up equal. Note that # default arguments are not available on the code objects. But if the # default arguments differ then the lambdas must also differ in other # ways, since default arguments are set up from bytecode and constants. # I.e., this appears to be safe wrt false positives. normalized_consts.append(f_const) else: normalized_consts.append(l_const) else: normalized_consts = l_consts return l.__code__.replace( co_code=bytes(co_code), co_consts=tuple(normalized_consts), ) _module_map: dict[int, str] = {} def _mimic_lambda_from_node(f, node): # Compile the source (represented by an ast.Lambda node) in a context that # as far as possible mimics the context that f was compiled in. If - and # only if - this was the source of f then the result is indistinguishable # from f itself (to a casual observer such as _function_key). f_globals = f.__globals__.copy() f_code = f.__code__ source = ast.unparse(node) # Install values for non-literal argument defaults. Thankfully, these are # always captured by value - so there is no interaction with the closure. if f.__defaults__: for f_default, l_default in zip( f.__defaults__, node.args.defaults, strict=True ): if isinstance(l_default, ast.Name): f_globals[l_default.id] = f_default if f.__kwdefaults__: # pragma: no cover for l_default, l_varname in zip( node.args.kw_defaults, node.args.kwonlyargs, strict=True ): if isinstance(l_default, ast.Name): f_globals[l_default.id] = f.__kwdefaults__[l_varname.arg] # CPython's compiler treats known imports differently than normal globals, # so check if we use attributes from globals that are modules (if so, we # import them explicitly and redundantly in the exec below) referenced_modules = [ (local_name, module) for attr in extract_all_attributes(node) if ( isinstance(attr.value, ast.Name) and (local_name := attr.value.id) and inspect.ismodule(module := f_globals.get(local_name)) ) ] if not f_code.co_freevars and not referenced_modules: compiled = eval(source, f_globals) else: if f_code.co_freevars: # We have to reconstruct a local closure. The closure will have # the same values as the original function, although this is not # required for source/bytecode equality. f_globals |= { f"__lc{i}": c.cell_contents for i, c in enumerate(f.__closure__) } captures = [f"{name}=__lc{i}" for i, name in enumerate(f_code.co_freevars)] capture_str = ";".join(captures) + ";" else: capture_str = "" if referenced_modules: # We add import statements for all referenced modules, since that # influences the compiled code. The assumption is that these modules # were explicitly imported, not assigned, in the source - if not, # this may/will give a different compilation result. global _module_map if len(_module_map) != len(sys.modules): # pragma: no branch _module_map = {id(module): name for name, module in sys.modules.items()} imports = [ (module_name, local_name) for local_name, module in referenced_modules if (module_name := _module_map.get(id(module))) is not None ] import_fragments = [f"{name} as {asname}" for name, asname in set(imports)] import_str = f"import {','.join(import_fragments)}\n" else: import_str = "" exec_str = ( f"{import_str}def __construct_lambda(): {capture_str} return ({source})" ) exec(exec_str, f_globals) compiled = f_globals["__construct_lambda"]() return compiled def _lambda_code_matches_node(f, node): try: compiled = _mimic_lambda_from_node(f, node) except (NameError, SyntaxError): # pragma: no cover # source is generated from ast return False if _function_key(f) == _function_key(compiled): return True # Try harder compiled.__code__ = _normalize_code(f, compiled) return _function_key(f) == _function_key(compiled) def _check_unknown_perfectly_aligned_lambda(candidate): # pragma: no cover # This is a monkeypatch point for our self-tests, to make unknown # lambdas raise. pass def _lambda_description(f, leeway=50, *, fail_if_confused_with_perfect_candidate=False): if hasattr(f, "__wrapped_target"): f = f.__wrapped_target # You might be wondering how a lambda can have a return-type annotation? # The answer is that we add this at runtime, in new_given_signature(), # and we do support strange choices as applying @given() to a lambda. sig = inspect.signature(f) assert sig.return_annotation in (Parameter.empty, None), sig # Using pytest-xdist on Python 3.13, there's an entry in the linecache for # file "", which then returns nonsense to getsource. Discard it. linecache.cache.pop("", None) def format_lambda(body): # The signature is more informative than the corresponding ast.unparse # output in the case of default argument values, so add the signature # to the unparsed body return ( f"lambda {str(sig)[1:-1]}: {body}" if sig.parameters else f"lambda: {body}" ) if_confused = format_lambda("") try: source_lines, lineno0 = inspect.findsource(f) source_lines = tuple(source_lines) # make it hashable except OSError: return if_confused try: all_lambdas = AST_LAMBDAS_CACHE[source_lines] except KeyError: # The source isn't already parsed, so we try to shortcut by parsing just # the local block. If that fails to produce a code-identical lambda, # fall through to the full parse. local_lines = inspect.getblock(source_lines[lineno0:]) local_block = textwrap.dedent("".join(local_lines)) # The fairly common ".map(lambda x: ...)" case. This partial block # isn't valid syntax, but it might be if we remove the leading ".". local_block = local_block.removeprefix(".") try: local_tree = ast.parse(local_block) except SyntaxError: pass else: local_lambdas = extract_all_lambdas(local_tree) for candidate in local_lambdas: if reflection.ast_arguments_matches_signature( candidate.args, sig ) and _lambda_code_matches_node(f, candidate): return format_lambda(ast.unparse(candidate.body)) # Local parse failed or didn't produce a match, go ahead with the full parse try: tree = ast.parse("".join(source_lines)) except SyntaxError: all_lambdas = [] else: all_lambdas = extract_all_lambdas(tree) AST_LAMBDAS_CACHE[source_lines] = all_lambdas aligned_lambdas = [] for candidate in all_lambdas: if ( candidate.lineno - leeway <= lineno0 + 1 <= candidate.lineno + leeway and reflection.ast_arguments_matches_signature(candidate.args, sig) ): aligned_lambdas.append(candidate) aligned_lambdas.sort(key=lambda c: abs(lineno0 + 1 - c.lineno)) for candidate in aligned_lambdas: if _lambda_code_matches_node(f, candidate): return format_lambda(ast.unparse(candidate.body)) # None of the aligned lambdas match perfectly in generated code. if aligned_lambdas and aligned_lambdas[0].lineno == lineno0 + 1: _check_unknown_perfectly_aligned_lambda(aligned_lambdas[0]) return if_confused def lambda_description(f): """ Returns a syntactically-valid expression describing `f`. This is often, but not always, the exact lambda definition string which appears in the source code. The difference comes from parsing the lambda ast into `tree` and then returning the result of `ast.unparse(tree)`, which may differ in whitespace, double vs single quotes, etc. Returns a string indicating an unknown body if the parsing gets confused in any way. """ try: return LAMBDA_DESCRIPTION_CACHE[f] except KeyError: pass key = _function_key(f, bounded_size=True) location = (f.__code__.co_filename, f.__code__.co_firstlineno) try: description, failed_locations = LAMBDA_DIGEST_DESCRIPTION_CACHE[key] except KeyError: failed_locations = set() else: # We got a hit in the digests cache, but only use it if either it has # a good (known) description, or if it is unknown but we already tried # to parse its exact source location before. if "" not in description or location in failed_locations: # use the cached result LAMBDA_DESCRIPTION_CACHE[f] = description return description description = _lambda_description(f) LAMBDA_DESCRIPTION_CACHE[f] = description if "" in description: failed_locations.add(location) else: failed_locations.clear() # we have a good description now LAMBDA_DIGEST_DESCRIPTION_CACHE[key] = description, failed_locations return description ================================================ FILE: hypothesis-python/src/hypothesis/internal/observability.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Observability tools to spit out analysis-ready tables, one row per test case.""" import base64 import dataclasses import json import math import os import sys import threading import time import warnings from collections.abc import Callable, Generator from contextlib import contextmanager from dataclasses import dataclass from datetime import date, timedelta from functools import lru_cache from threading import Lock from typing import ( TYPE_CHECKING, Any, Literal, Optional, TypeAlias, Union, cast, ) from hypothesis.configuration import storage_directory from hypothesis.errors import HypothesisWarning from hypothesis.internal.conjecture.choice import ( BooleanConstraints, BytesConstraints, ChoiceConstraintsT, ChoiceNode, ChoiceT, ChoiceTypeT, FloatConstraints, IntegerConstraints, StringConstraints, ) from hypothesis.internal.escalation import InterestingOrigin from hypothesis.internal.floats import float_to_int from hypothesis.internal.intervalsets import IntervalSet from hypothesis.utils.deprecation import note_deprecation if TYPE_CHECKING: from hypothesis.internal.conjecture.data import ConjectureData, Spans, Status Observation: TypeAlias = Union["InfoObservation", "TestCaseObservation"] CallbackThreadT: TypeAlias = Callable[[Observation], None] # for all_threads=True, we pass the thread id as well. CallbackAllThreadsT: TypeAlias = Callable[[Observation, int], None] CallbackT: TypeAlias = CallbackThreadT | CallbackAllThreadsT # thread_id: list[callback] _callbacks: dict[int | None, list[CallbackThreadT]] = {} # callbacks where all_threads=True was set _callbacks_all_threads: list[CallbackAllThreadsT] = [] @dataclass(slots=True, frozen=False) class PredicateCounts: satisfied: int = 0 unsatisfied: int = 0 def update_count(self, *, condition: bool) -> None: if condition: self.satisfied += 1 else: self.unsatisfied += 1 def _choice_to_json(choice: ChoiceT | None) -> Any: if choice is None: return None # see the note on the same check in to_jsonable for why we cast large # integers to floats. if ( isinstance(choice, int) and not isinstance(choice, bool) and abs(choice) >= 2**63 ): return ["integer", str(choice)] elif isinstance(choice, bytes): return ["bytes", base64.b64encode(choice).decode()] elif isinstance(choice, float) and math.isnan(choice): # handle nonstandard nan bit patterns. We don't need to do this for -0.0 # vs 0.0 since json doesn't normalize -0.0 to 0.0. return ["float", float_to_int(choice)] return choice def choices_to_json(choices: tuple[ChoiceT, ...]) -> list[Any]: return [_choice_to_json(choice) for choice in choices] def _constraints_to_json( choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT ) -> dict[str, Any]: constraints = constraints.copy() if choice_type == "integer": constraints = cast(IntegerConstraints, constraints) return { "min_value": _choice_to_json(constraints["min_value"]), "max_value": _choice_to_json(constraints["max_value"]), "weights": ( None if constraints["weights"] is None # wrap up in a list, instead of a dict, because json dicts # require string keys else [ (_choice_to_json(k), v) for k, v in constraints["weights"].items() ] ), "shrink_towards": _choice_to_json(constraints["shrink_towards"]), } elif choice_type == "float": constraints = cast(FloatConstraints, constraints) return { "min_value": _choice_to_json(constraints["min_value"]), "max_value": _choice_to_json(constraints["max_value"]), "allow_nan": constraints["allow_nan"], "smallest_nonzero_magnitude": constraints["smallest_nonzero_magnitude"], } elif choice_type == "string": constraints = cast(StringConstraints, constraints) assert isinstance(constraints["intervals"], IntervalSet) return { "intervals": constraints["intervals"].intervals, "min_size": _choice_to_json(constraints["min_size"]), "max_size": _choice_to_json(constraints["max_size"]), } elif choice_type == "bytes": constraints = cast(BytesConstraints, constraints) return { "min_size": _choice_to_json(constraints["min_size"]), "max_size": _choice_to_json(constraints["max_size"]), } elif choice_type == "boolean": constraints = cast(BooleanConstraints, constraints) return { "p": constraints["p"], } else: raise NotImplementedError(f"unknown choice type {choice_type}") def nodes_to_json(nodes: tuple[ChoiceNode, ...]) -> list[dict[str, Any]]: return [ { "type": node.type, "value": _choice_to_json(node.value), "constraints": _constraints_to_json(node.type, node.constraints), "was_forced": node.was_forced, } for node in nodes ] @dataclass(slots=True, frozen=True) class ObservationMetadata: traceback: str | None reproduction_decorator: str | None predicates: dict[str, PredicateCounts] backend: dict[str, Any] sys_argv: list[str] os_getpid: int imported_at: float data_status: "Status" phase: str interesting_origin: InterestingOrigin | None choice_nodes: tuple[ChoiceNode, ...] | None choice_spans: Optional["Spans"] def to_json(self) -> dict[str, Any]: data = { "traceback": self.traceback, "reproduction_decorator": self.reproduction_decorator, "predicates": self.predicates, "backend": self.backend, "sys.argv": self.sys_argv, "os.getpid()": self.os_getpid, "imported_at": self.imported_at, "data_status": self.data_status, "phase": self.phase, "interesting_origin": self.interesting_origin, "choice_nodes": ( None if self.choice_nodes is None else nodes_to_json(self.choice_nodes) ), "choice_spans": ( None if self.choice_spans is None else [ ( # span.label is an int, but cast to string to avoid conversion # to float (and loss of precision) for large label values. # # The value of this label is opaque to consumers anyway, so its # type shouldn't matter as long as it's consistent. str(span.label), span.start, span.end, span.discarded, ) for span in self.choice_spans ] ), } # check that we didn't forget one assert len(data) == len(dataclasses.fields(self)) return data @dataclass(slots=True, frozen=True) class BaseObservation: type: Literal["test_case", "info", "alert", "error"] property: str run_start: float InfoObservationType = Literal["info", "alert", "error"] TestCaseStatus = Literal["gave_up", "passed", "failed"] @dataclass(slots=True, frozen=True) class InfoObservation(BaseObservation): type: InfoObservationType title: str content: str | dict @dataclass(slots=True, frozen=True) class TestCaseObservation(BaseObservation): __test__ = False # no! bad pytest! type: Literal["test_case"] status: TestCaseStatus status_reason: str representation: str arguments: dict how_generated: str features: dict coverage: dict[str, list[int]] | None timing: dict[str, float] metadata: ObservationMetadata def add_observability_callback(f: CallbackT, /, *, all_threads: bool = False) -> None: """ Adds ``f`` as a callback for :ref:`observability `. ``f`` should accept one argument, which is an observation. Whenever Hypothesis produces a new observation, it calls each callback with that observation. If Hypothesis tests are being run from multiple threads, callbacks are tracked per-thread. In other words, ``add_observability_callback(f)`` only adds ``f`` as an observability callback for observations produced on that thread. If ``all_threads=True`` is passed, ``f`` will instead be registered as a callback for all threads. This means it will be called for observations generated by all threads, not just the thread which registered ``f`` as a callback. In this case, ``f`` will be passed two arguments: the first is the observation, and the second is the integer thread id from :func:`python:threading.get_ident` where that observation was generated. We recommend against registering ``f`` as a callback for both ``all_threads=True`` and the default ``all_threads=False``, due to unclear semantics with |remove_observability_callback|. """ if all_threads: _callbacks_all_threads.append(cast(CallbackAllThreadsT, f)) return thread_id = threading.get_ident() if thread_id not in _callbacks: _callbacks[thread_id] = [] _callbacks[thread_id].append(cast(CallbackThreadT, f)) def remove_observability_callback(f: CallbackT, /) -> None: """ Removes ``f`` from the :ref:`observability ` callbacks. If ``f`` is not in the list of observability callbacks, silently do nothing. If running under multiple threads, ``f`` will only be removed from the callbacks for this thread. """ if f in _callbacks_all_threads: _callbacks_all_threads.remove(cast(CallbackAllThreadsT, f)) thread_id = threading.get_ident() if thread_id not in _callbacks: return callbacks = _callbacks[thread_id] if f in callbacks: callbacks.remove(cast(CallbackThreadT, f)) if not callbacks: del _callbacks[thread_id] def observability_enabled() -> bool: """ Returns whether or not Hypothesis considers :ref:`observability ` to be enabled. Observability is enabled if there is at least one observability callback present. Callers might use this method to determine whether they should compute an expensive representation that is only used under observability, for instance by |alternative backends|. """ return bool(_callbacks) or bool(_callbacks_all_threads) @contextmanager def with_observability_callback( f: Callable[[Observation], None], /, *, all_threads: bool = False ) -> Generator[None, None, None]: """ A simple context manager which calls |add_observability_callback| on ``f`` when it enters and |remove_observability_callback| on ``f`` when it exits. """ add_observability_callback(f, all_threads=all_threads) try: yield finally: remove_observability_callback(f) def deliver_observation(observation: Observation) -> None: thread_id = threading.get_ident() for callback in _callbacks.get(thread_id, []): callback(observation) for callback in _callbacks_all_threads: callback(observation, thread_id) class _TestcaseCallbacks: def __bool__(self): self._note_deprecation() return bool(_callbacks) def _note_deprecation(self): note_deprecation( "hypothesis.internal.observability.TESTCASE_CALLBACKS is deprecated. " "Replace TESTCASE_CALLBACKS.append with add_observability_callback, " "TESTCASE_CALLBACKS.remove with remove_observability_callback, and " "bool(TESTCASE_CALLBACKS) with observability_enabled().", since="2025-08-01", has_codemod=False, ) def append(self, f): self._note_deprecation() add_observability_callback(f) def remove(self, f): self._note_deprecation() remove_observability_callback(f) #: .. warning:: #: #: Deprecated in favor of |add_observability_callback|, #: |remove_observability_callback|, and |observability_enabled|. #: #: |TESTCASE_CALLBACKS| remains a thin compatibility #: shim which forwards ``.append``, ``.remove``, and ``bool()`` to those #: three methods. It is not an attempt to be fully compatible with the previous #: ``TESTCASE_CALLBACKS = []``, so iteration or other usages will not work #: anymore. Please update to using the new methods instead. #: #: |TESTCASE_CALLBACKS| will eventually be removed. TESTCASE_CALLBACKS = _TestcaseCallbacks() def make_testcase( *, run_start: float, property: str, data: "ConjectureData", how_generated: str, representation: str = "", timing: dict[str, float], arguments: dict | None = None, coverage: dict[str, list[int]] | None = None, phase: str | None = None, backend_metadata: dict[str, Any] | None = None, status: ( Union[TestCaseStatus, "Status"] | None ) = None, # overrides automatic calculation status_reason: str | None = None, # overrides automatic calculation # added to calculated metadata. If keys overlap, the value from this `metadata` # is used metadata: dict[str, Any] | None = None, ) -> TestCaseObservation: from hypothesis.core import reproduction_decorator from hypothesis.internal.conjecture.data import Status # We should only be sending observability reports for datas that have finished # being modified. assert data.frozen if status_reason is not None: pass elif data.interesting_origin: status_reason = str(data.interesting_origin) elif phase == "shrink" and data.status == Status.OVERRUN: status_reason = "exceeded size of current best example" else: status_reason = str(data.events.pop("invalid because", "")) status_map: dict[Status, TestCaseStatus] = { Status.OVERRUN: "gave_up", Status.INVALID: "gave_up", Status.VALID: "passed", Status.INTERESTING: "failed", } if status is not None and isinstance(status, Status): status = status_map[status] if status is None: status = status_map[data.status] return TestCaseObservation( type="test_case", status=status, status_reason=status_reason, representation=representation, arguments={ k.removeprefix("generate:"): v for k, v in (arguments or {}).items() }, how_generated=how_generated, # iid, mutation, etc. features={ **{ f"target:{k}".strip(":"): v for k, v in data.target_observations.items() }, **data.events, }, coverage=coverage, timing=timing, metadata=ObservationMetadata( **{ "traceback": data.expected_traceback, "reproduction_decorator": ( reproduction_decorator(data.choices) if status == "failed" else None ), "predicates": dict(data._observability_predicates), "backend": backend_metadata or {}, "data_status": data.status, "phase": phase, "interesting_origin": data.interesting_origin, "choice_nodes": data.nodes if OBSERVABILITY_CHOICES else None, "choice_spans": data.spans if OBSERVABILITY_CHOICES else None, **_system_metadata(), # unpack last so it takes precedence for duplicate keys **(metadata or {}), } ), run_start=run_start, property=property, ) _WROTE_TO = set() _deliver_to_file_lock = Lock() def _deliver_to_file( observation: Observation, thread_id: int ) -> None: # pragma: no cover from hypothesis.strategies._internal.utils import to_jsonable kind = "testcases" if observation.type == "test_case" else "info" fname = storage_directory("observed", f"{date.today().isoformat()}_{kind}.jsonl") fname.parent.mkdir(exist_ok=True, parents=True) observation_bytes = ( json.dumps(to_jsonable(observation, avoid_realization=False)) + "\n" ) # only allow one conccurent file write to avoid write races. This is likely to make # HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY quite slow under threading. A queue # would be an improvement, but that requires a background thread, and I # would prefer to avoid a thread in the single-threaded case. We could # switch over to a queue if we detect multithreading, but it's tricky to get # right. with _deliver_to_file_lock: _WROTE_TO.add(fname) with fname.open(mode="a") as f: f.write(observation_bytes) _imported_at = time.time() @lru_cache def _system_metadata() -> dict[str, Any]: return { "sys_argv": sys.argv, "os_getpid": os.getpid(), "imported_at": _imported_at, } #: If ``False``, do not collect coverage information when observability is enabled. #: #: This is exposed both for performance (as coverage collection can be slow on #: Python 3.11 and earlier) and size (if you do not use coverage information, #: you may not want to store it in-memory). OBSERVABILITY_COLLECT_COVERAGE = ( "HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY_NOCOVER" not in os.environ ) #: If ``True``, include the ``metadata.choice_nodes`` and ``metadata.spans`` keys #: in test case observations. #: #: ``False`` by default. ``metadata.choice_nodes`` and ``metadata.spans`` can be #: a substantial amount of data, and so must be opted-in to, even when #: observability is enabled. #: #: .. warning:: #: #: EXPERIMENTAL AND UNSTABLE. We are actively working towards a better #: interface for this as of June 2025, and this attribute may disappear or #: be renamed without notice. #: OBSERVABILITY_CHOICES = "HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY_CHOICES" in os.environ if OBSERVABILITY_COLLECT_COVERAGE is False and ( sys.version_info[:2] >= (3, 12) ): # pragma: no cover warnings.warn( "Coverage data collection should be quite fast in Python 3.12 or later " "so there should be no need to turn coverage reporting off.", HypothesisWarning, stacklevel=2, ) if ( "HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY" in os.environ or OBSERVABILITY_COLLECT_COVERAGE is False ): # pragma: no cover add_observability_callback(_deliver_to_file, all_threads=True) # Remove files more than a week old, to cap the size on disk max_age = (date.today() - timedelta(days=8)).isoformat() for f in storage_directory("observed", intent_to_write=False).glob("*.jsonl"): if f.stem < max_age: # pragma: no branch f.unlink(missing_ok=True) ================================================ FILE: hypothesis-python/src/hypothesis/internal/reflection.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This file can approximately be considered the collection of hypothesis going to really unreasonable lengths to produce pretty output.""" import ast import hashlib import inspect import re import textwrap import types import warnings from collections.abc import Callable, Sequence from functools import partial, wraps from inspect import Parameter, Signature from io import StringIO from keyword import iskeyword from random import _inst as global_random_instance from tokenize import COMMENT, generate_tokens, untokenize from types import EllipsisType, ModuleType from typing import TYPE_CHECKING, Any, TypeVar, Union from unittest.mock import _patch as PatchType from hypothesis.errors import HypothesisWarning from hypothesis.internal import lambda_sources from hypothesis.internal.compat import is_typed_named_tuple from hypothesis.utils.conventions import not_set from hypothesis.vendor.pretty import pretty if TYPE_CHECKING: from hypothesis.strategies._internal.strategies import SearchStrategy T = TypeVar("T") def is_mock(obj: object) -> bool: """Determine if the given argument is a mock type.""" # We want to be able to detect these when dealing with various test # args. As they are sneaky and can look like almost anything else, # we'll check this by looking for an attribute with a name that it's really # unlikely to implement accidentally, and that anyone who implements it # deliberately should know what they're doing. This is more robust than # looking for types. return hasattr(obj, "hypothesis_internal_is_this_a_mock_check") def _clean_source(src: str) -> bytes: """Return the source code as bytes, without decorators or comments. Because this is part of our database key, we reduce the cache invalidation rate by ignoring decorators, comments, trailing whitespace, and empty lines. We can't just use the (dumped) AST directly because it changes between Python versions (e.g. ast.Constant) """ # Get the (one-indexed) line number of the function definition, and drop preceding # lines - i.e. any decorators, so that adding `@example()`s keeps the same key. try: funcdef = ast.parse(src).body[0] src = "".join(src.splitlines(keepends=True)[funcdef.lineno - 1 :]) except Exception: pass # Remove blank lines and use the tokenize module to strip out comments, # so that those can be changed without changing the database key. try: src = untokenize( t for t in generate_tokens(StringIO(src).readline) if t.type != COMMENT ) except Exception: pass # Finally, remove any trailing whitespace and empty lines as a last cleanup. return "\n".join(x.rstrip() for x in src.splitlines() if x.rstrip()).encode() def function_digest(function: Any) -> bytes: """Returns a string that is stable across multiple invocations across multiple processes and is prone to changing significantly in response to minor changes to the function. No guarantee of uniqueness though it usually will be. Digest collisions lead to unfortunate but not fatal problems during database replay. """ hasher = hashlib.sha384() try: src = inspect.getsource(function) except (OSError, TypeError): # If we can't actually get the source code, try for the name as a fallback. # NOTE: We might want to change this to always adding function.__qualname__, # to differentiate f.x. two classes having the same function implementation # with class-dependent behaviour. try: hasher.update(function.__name__.encode()) except AttributeError: pass else: hasher.update(_clean_source(src)) try: # This is additional to the source code because it can include the effects # of decorators, or of post-hoc assignment to the .__signature__ attribute. hasher.update(repr(get_signature(function)).encode()) except Exception: pass try: # We set this in order to distinguish e.g. @pytest.mark.parametrize cases. hasher.update(function._hypothesis_internal_add_digest) except AttributeError: pass return hasher.digest() def check_signature(sig: Signature) -> None: # pragma: no cover # 3.10 only # Backport from Python 3.11; see https://github.com/python/cpython/pull/92065 for p in sig.parameters.values(): if iskeyword(p.name) and p.kind is not p.POSITIONAL_ONLY: raise ValueError( f"Signature {sig!r} contains a parameter named {p.name!r}, " f"but this is a SyntaxError because `{p.name}` is a keyword. " "You, or a library you use, must have manually created an " "invalid signature - this will be an error in Python 3.11+" ) def get_signature( target: Any, *, follow_wrapped: bool = True, eval_str: bool = False ) -> Signature: # Special case for use of `@unittest.mock.patch` decorator, mimicking the # behaviour of getfullargspec instead of reporting unusable arguments. patches = getattr(target, "patchings", None) if isinstance(patches, list) and all(isinstance(p, PatchType) for p in patches): return Signature( [ Parameter("args", Parameter.VAR_POSITIONAL), Parameter("keywargs", Parameter.VAR_KEYWORD), ] ) if isinstance(getattr(target, "__signature__", None), Signature): # This special case covers unusual codegen like Pydantic models sig = target.__signature__ check_signature(sig) # And *this* much more complicated block ignores the `self` argument # if that's been (incorrectly) included in the custom signature. if sig.parameters and (inspect.isclass(target) or inspect.ismethod(target)): selfy = next(iter(sig.parameters.values())) if ( selfy.name == "self" and selfy.default is Parameter.empty and selfy.kind.name.startswith("POSITIONAL_") ): return sig.replace( parameters=[v for k, v in sig.parameters.items() if k != "self"] ) return sig sig = inspect.signature(target, follow_wrapped=follow_wrapped, eval_str=eval_str) check_signature(sig) return sig def arg_is_required(param: Parameter) -> bool: return param.default is Parameter.empty and param.kind in ( Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY, ) def required_args( target: Callable[..., Any], args: tuple["SearchStrategy[Any]", ...] = (), kwargs: dict[str, Union["SearchStrategy[Any]", EllipsisType]] | None = None, ) -> set[str]: """Return a set of names of required args to target that were not supplied in args or kwargs. This is used in builds() to determine which arguments to attempt to fill from type hints. target may be any callable (including classes and bound methods). args and kwargs should be as they are passed to builds() - that is, a tuple of values and a dict of names: values. """ kwargs = {} if kwargs is None else kwargs # We start with a workaround for NamedTuples, which don't have nice inits if inspect.isclass(target) and is_typed_named_tuple(target): provided = set(kwargs) | set(target._fields[: len(args)]) return set(target._fields) - provided # Then we try to do the right thing with inspect.signature try: sig = get_signature(target) except (ValueError, TypeError): return set() return { name for name, param in list(sig.parameters.items())[len(args) :] if arg_is_required(param) and name not in kwargs } def convert_keyword_arguments( function: Any, args: Sequence[object], kwargs: dict[str, object] ) -> tuple[tuple[object, ...], dict[str, object]]: """Returns a pair of a tuple and a dictionary which would be equivalent passed as positional and keyword args to the function. Unless function has kwonlyargs or **kwargs the dictionary will always be empty. """ sig = inspect.signature(function, follow_wrapped=False) bound = sig.bind(*args, **kwargs) return bound.args, bound.kwargs def convert_positional_arguments( function: Any, args: Sequence[object], kwargs: dict[str, object] ) -> tuple[tuple[object, ...], dict[str, object]]: """Return a tuple (new_args, new_kwargs) where all possible arguments have been moved to kwargs. new_args will only be non-empty if function has pos-only args or *args. """ sig = inspect.signature(function, follow_wrapped=False) bound = sig.bind(*args, **kwargs) new_args = [] new_kwargs = dict(bound.arguments) for p in sig.parameters.values(): if p.name in new_kwargs: if p.kind is p.POSITIONAL_ONLY: new_args.append(new_kwargs.pop(p.name)) elif p.kind is p.VAR_POSITIONAL: new_args.extend(new_kwargs.pop(p.name)) elif p.kind is p.VAR_KEYWORD: assert set(new_kwargs[p.name]).isdisjoint(set(new_kwargs) - {p.name}) new_kwargs.update(new_kwargs.pop(p.name)) return tuple(new_args), new_kwargs def ast_arguments_matches_signature(args: ast.arguments, sig: Signature) -> bool: expected: list[tuple[str, int]] = [] for node in args.posonlyargs: expected.append((node.arg, Parameter.POSITIONAL_ONLY)) for node in args.args: expected.append((node.arg, Parameter.POSITIONAL_OR_KEYWORD)) if args.vararg is not None: expected.append((args.vararg.arg, Parameter.VAR_POSITIONAL)) for node in args.kwonlyargs: expected.append((node.arg, Parameter.KEYWORD_ONLY)) if args.kwarg is not None: expected.append((args.kwarg.arg, Parameter.VAR_KEYWORD)) return expected == [(p.name, p.kind) for p in sig.parameters.values()] def is_first_param_referenced_in_function(f: Any) -> bool: """Is the given name referenced within f?""" try: tree = ast.parse(textwrap.dedent(inspect.getsource(f))) except Exception: return True # Assume it's OK unless we know otherwise name = next(iter(get_signature(f).parameters)) return any( isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Load) for node in ast.walk(tree) ) def get_pretty_function_description(f: object) -> str: if isinstance(f, partial): return pretty(f) if not hasattr(f, "__name__"): return repr(f) name = f.__name__ # type: ignore if name == "": return lambda_sources.lambda_description(f) elif isinstance(f, (types.MethodType, types.BuiltinMethodType)): self = f.__self__ # Some objects, like `builtins.abs` are of BuiltinMethodType but have # their module as __self__. This might include c-extensions generally? if not (self is None or inspect.isclass(self) or inspect.ismodule(self)): if self is global_random_instance: return f"random.{name}" return f"{self!r}.{name}" elif isinstance(name, str) and getattr(dict, name, object()) is f: # special case for keys/values views in from_type() / ghostwriter output return f"dict.{name}" return name def nicerepr(v: Any) -> str: if inspect.isfunction(v): return get_pretty_function_description(v) elif isinstance(v, type): return v.__name__ else: # With TypeVar T, show List[T] instead of TypeError on List[~T] return re.sub(r"(\[)~([A-Z][a-z]*\])", r"\g<1>\g<2>", pretty(v)) def repr_call( f: Any, args: Sequence[object], kwargs: dict[str, object], *, reorder: bool = True ) -> str: # Note: for multi-line pretty-printing, see RepresentationPrinter.repr_call() if reorder: args, kwargs = convert_positional_arguments(f, args, kwargs) bits = [nicerepr(x) for x in args] for p in get_signature(f).parameters.values(): if p.name in kwargs and not p.kind.name.startswith("VAR_"): bits.append(f"{p.name}={nicerepr(kwargs.pop(p.name))}") if kwargs: for a in sorted(kwargs): bits.append(f"{a}={nicerepr(kwargs[a])}") rep = nicerepr(f) if rep.startswith("lambda") and ":" in rep: rep = f"({rep})" repr_len = len(rep) + sum(len(b) for b in bits) # approx if repr_len > 30000: warnings.warn( "Generating overly large repr. This is an expensive operation, and with " f"a length of {repr_len//1000} kB is unlikely to be useful. Use -Wignore " "to ignore the warning, or -Werror to get a traceback.", HypothesisWarning, stacklevel=2, ) return rep + "(" + ", ".join(bits) + ")" def check_valid_identifier(identifier: str) -> None: if not identifier.isidentifier(): raise ValueError(f"{identifier!r} is not a valid python identifier") eval_cache: dict[str, ModuleType] = {} def source_exec_as_module(source: str) -> ModuleType: try: return eval_cache[source] except KeyError: pass hexdigest = hashlib.sha384(source.encode()).hexdigest() result = ModuleType("hypothesis_temporary_module_" + hexdigest) assert isinstance(source, str) exec(source, result.__dict__) eval_cache[source] = result return result COPY_SIGNATURE_SCRIPT = """ from hypothesis.utils.conventions import not_set def accept({funcname}): def {name}{signature}: return {funcname}({invocation}) return {name} """.lstrip() def get_varargs( sig: Signature, kind: int = Parameter.VAR_POSITIONAL ) -> Parameter | None: for p in sig.parameters.values(): if p.kind is kind: return p return None def define_function_signature(name, docstring, signature): """A decorator which sets the name, signature and docstring of the function passed into it.""" if name == "": name = "_lambda_" check_valid_identifier(name) for a in signature.parameters: check_valid_identifier(a) used_names = {*signature.parameters, name} newsig = signature.replace( parameters=[ p if p.default is signature.empty else p.replace(default=not_set) for p in ( p.replace(annotation=signature.empty) for p in signature.parameters.values() ) ], return_annotation=signature.empty, ) pos_args = [ p for p in signature.parameters.values() if p.kind.name.startswith("POSITIONAL_") ] def accept(f): fsig = inspect.signature(f, follow_wrapped=False) must_pass_as_kwargs = [] invocation_parts = [] for p in pos_args: if p.name not in fsig.parameters and get_varargs(fsig) is None: must_pass_as_kwargs.append(p.name) else: invocation_parts.append(p.name) if get_varargs(signature) is not None: invocation_parts.append("*" + get_varargs(signature).name) for k in must_pass_as_kwargs: invocation_parts.append(f"{k}={k}") for p in signature.parameters.values(): if p.kind is p.KEYWORD_ONLY: invocation_parts.append(f"{p.name}={p.name}") varkw = get_varargs(signature, kind=Parameter.VAR_KEYWORD) if varkw: invocation_parts.append("**" + varkw.name) candidate_names = ["f"] + [f"f_{i}" for i in range(1, len(used_names) + 2)] for funcname in candidate_names: # pragma: no branch if funcname not in used_names: break source = COPY_SIGNATURE_SCRIPT.format( name=name, funcname=funcname, signature=str(newsig), invocation=", ".join(invocation_parts), ) result = source_exec_as_module(source).accept(f) result.__doc__ = docstring result.__defaults__ = tuple( p.default for p in signature.parameters.values() if p.default is not signature.empty and "POSITIONAL" in p.kind.name ) kwdefaults = { p.name: p.default for p in signature.parameters.values() if p.default is not signature.empty and p.kind is p.KEYWORD_ONLY } if kwdefaults: result.__kwdefaults__ = kwdefaults annotations = { p.name: p.annotation for p in signature.parameters.values() if p.annotation is not signature.empty } if signature.return_annotation is not signature.empty: annotations["return"] = signature.return_annotation if annotations: result.__annotations__ = annotations return result return accept def impersonate(target): """Decorator to update the attributes of a function so that to external introspectors it will appear to be the target function. Note that this updates the function in place, it doesn't return a new one. """ def accept(f): # Lie shamelessly about where this code comes from, to hide the hypothesis # internals from pytest, ipython, and other runtime introspection. f.__code__ = f.__code__.replace( co_filename=target.__code__.co_filename, co_firstlineno=target.__code__.co_firstlineno, ) f.__name__ = target.__name__ f.__module__ = target.__module__ f.__doc__ = target.__doc__ f.__globals__["__hypothesistracebackhide__"] = True # But leave an breadcrumb for _describe_lambda to follow, it's # just confused by the lies above f.__wrapped_target = target return f return accept def proxies(target: T) -> Callable[[Callable], T]: replace_sig = define_function_signature( target.__name__.replace("", "_lambda_"), # type: ignore target.__doc__, get_signature(target, follow_wrapped=False), ) def accept(proxy): return impersonate(target)(wraps(target)(replace_sig(proxy))) return accept def is_identity_function(f: Callable) -> bool: try: code = f.__code__ except AttributeError: try: f = f.__call__ # type: ignore code = f.__code__ except AttributeError: return False # We only accept a single unbound argument. While it would be possible to # accept extra defaulted arguments, it would be pointless as they couldn't # be referenced at all in the code object (or the co_code check would fail). bound_args = int(inspect.ismethod(f)) if code.co_argcount != bound_args + 1 or code.co_kwonlyargcount > 0: return False # We know that f accepts a single positional argument, now check that its # code object is simply "return first unbound argument". template = (lambda self, x: x) if bound_args else (lambda x: x) # type: ignore try: return code.co_code == template.__code__.co_code except AttributeError: # pragma: no cover # pypy only # In PyPy, some builtin functions have a code object ('builtin-code') # lacking co_code, perhaps because they are native-compiled and don't have # a corresponding bytecode. Regardless, since Python doesn't have any # builtin identity function it seems safe to say that this one isn't return False ================================================ FILE: hypothesis-python/src/hypothesis/internal/scrutineer.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import functools import os import re import subprocess import sys import sysconfig import types from collections import defaultdict from collections.abc import Iterable from enum import IntEnum from functools import lru_cache, reduce from os import sep from pathlib import Path from typing import TYPE_CHECKING, TypeAlias from hypothesis._settings import Phase, Verbosity from hypothesis.internal.compat import PYPY from hypothesis.internal.escalation import is_hypothesis_file if TYPE_CHECKING: from typing_extensions import Self Location: TypeAlias = tuple[str, int] Branch: TypeAlias = tuple[Location | None, Location] Trace: TypeAlias = frozenset[Branch] @functools.cache def should_trace_file(fname: str) -> bool: # fname.startswith("<") indicates runtime code-generation via compile, # e.g. compile("def ...", "", "exec") in e.g. attrs methods. return not (is_hypothesis_file(fname) or fname.startswith("<")) # where possible, we'll use 3.12's new sys.monitoring module for low-overhead # coverage instrumentation; on older python versions we'll use sys.settrace. # tool_id = 1 is designated for coverage, but we intentionally choose a # non-reserved tool id so we can co-exist with coverage tools. MONITORING_TOOL_ID = 3 if hasattr(sys, "monitoring"): MONITORING_EVENTS = {sys.monitoring.events.LINE: "trace_line"} class Tracer: """A super-simple branch coverage tracer.""" __slots__ = ( "_branches", "_previous_location", "_should_trace", "_tried_and_failed_to_trace", ) def __init__(self, *, should_trace: bool) -> None: self._branches: set[Branch] = set() self._previous_location: Location | None = None self._tried_and_failed_to_trace = False self._should_trace = should_trace and self.can_trace() @staticmethod def can_trace() -> bool: if PYPY: return False if hasattr(sys, "monitoring"): return sys.monitoring.get_tool(MONITORING_TOOL_ID) is None return sys.gettrace() is None @property def branches(self) -> Trace: return frozenset(self._branches) def trace(self, frame, event, arg): try: if event == "call": return self.trace elif event == "line": fname = frame.f_code.co_filename if should_trace_file(fname): current_location = (fname, frame.f_lineno) self._branches.add((self._previous_location, current_location)) self._previous_location = current_location except RecursionError: pass def trace_line(self, code: types.CodeType, line_number: int) -> None: fname = code.co_filename if not should_trace_file(fname): # this function is only called on 3.12+, but we want to avoid an # assertion to that effect for performance. return sys.monitoring.DISABLE # type: ignore current_location = (fname, line_number) self._branches.add((self._previous_location, current_location)) self._previous_location = current_location def __enter__(self) -> "Self": self._tried_and_failed_to_trace = False if not self._should_trace: return self if not hasattr(sys, "monitoring"): sys.settrace(self.trace) return self try: sys.monitoring.use_tool_id(MONITORING_TOOL_ID, "scrutineer") except ValueError: # another thread may have registered a tool for MONITORING_TOOL_ID # since we checked in can_trace. self._tried_and_failed_to_trace = True return self for event, callback_name in MONITORING_EVENTS.items(): sys.monitoring.set_events(MONITORING_TOOL_ID, event) callback = getattr(self, callback_name) sys.monitoring.register_callback(MONITORING_TOOL_ID, event, callback) return self def __exit__(self, *args, **kwargs): if not self._should_trace: return if not hasattr(sys, "monitoring"): sys.settrace(None) return if self._tried_and_failed_to_trace: return sys.monitoring.free_tool_id(MONITORING_TOOL_ID) for event in MONITORING_EVENTS: sys.monitoring.register_callback(MONITORING_TOOL_ID, event, None) UNHELPFUL_LOCATIONS = ( # There's a branch which is only taken when an exception is active while exiting # a contextmanager; this is probably after the fault has been triggered. # Similar reasoning applies to a few other standard-library modules: even # if the fault was later, these still aren't useful locations to report! # Note: The list is post-processed, so use plain "/" for separator here. "/contextlib.py", "/inspect.py", "/re.py", "/re/__init__.py", # refactored in Python 3.11 "/warnings.py", # Quite rarely, the first AFNP line is in Pytest's internals. "/_pytest/**", "/pluggy/_*.py", # used by pytest for failure formatting in the terminal. # seen: pygments/lexer.py, pygments/formatters/, pygments/filter.py. "/pygments/*", # used by pytest for failure formatting "/difflib.py", "/reprlib.py", "/typing.py", "/conftest.py", "/pprint.py", ) def _glob_to_re(locs: Iterable[str]) -> str: """Translate a list of glob patterns to a combined regular expression. Only the * and ** wildcards are supported, and patterns including special characters will only work by chance.""" # fnmatch.translate is not an option since its "*" consumes path sep return "|".join( loc.replace(".", re.escape(".")) .replace("**", r".+") .replace("*", r"[^/]+") .replace("/", re.escape(sep)) + r"\Z" # right anchored for loc in locs ) def get_explaining_locations(traces): # Traces is a dict[interesting_origin | None, set[frozenset[tuple[str, int]]]] # Each trace in the set might later become a Counter instead of frozenset. if not traces: return {} unions = {origin: set().union(*values) for origin, values in traces.items()} seen_passing = {None}.union(*unions.pop(None, set())) always_failing_never_passing = { origin: reduce(set.intersection, [set().union(*v) for v in values]) - seen_passing for origin, values in traces.items() if origin is not None } # Build the observed parts of the control-flow graph for each origin cf_graphs = {origin: defaultdict(set) for origin in unions} for origin, seen_arcs in unions.items(): for src, dst in seen_arcs: cf_graphs[origin][src].add(dst) assert cf_graphs[origin][None], "Expected start node with >=1 successor" # For each origin, our explanation is the always_failing_never_passing lines # which are reachable from the start node (None) without passing through another # AFNP line. So here's a whatever-first search with early stopping: explanations = defaultdict(set) for origin in unions: queue = {None} seen = set() while queue: assert queue.isdisjoint(seen), f"Intersection: {queue & seen}" src = queue.pop() seen.add(src) if src in always_failing_never_passing[origin]: explanations[origin].add(src) else: queue.update(cf_graphs[origin][src] - seen) # The last step is to filter out explanations that we know would be uninformative. # When this is the first AFNP location, we conclude that Scrutineer missed the # real divergence (earlier in the trace) and drop that unhelpful explanation. filter_regex = re.compile(_glob_to_re(UNHELPFUL_LOCATIONS)) return { origin: {loc for loc in afnp_locs if not filter_regex.search(loc[0])} for origin, afnp_locs in explanations.items() } # see e.g. https://docs.python.org/3/library/sysconfig.html#posix-user # for examples of these path schemes STDLIB_DIRS = { Path(sysconfig.get_path("platstdlib")).resolve(), Path(sysconfig.get_path("stdlib")).resolve(), } SITE_PACKAGES_DIRS = { Path(sysconfig.get_path("purelib")).resolve(), Path(sysconfig.get_path("platlib")).resolve(), } EXPLANATION_STUB = ( "Explanation:", " These lines were always and only run by failing examples:", ) class ModuleLocation(IntEnum): LOCAL = 0 SITE_PACKAGES = 1 STDLIB = 2 @classmethod @lru_cache(1024) def from_path(cls, path: str) -> "ModuleLocation": path = Path(path).resolve() # site-packages may be a subdir of stdlib or platlib, so it's important to # check is_relative_to for this before the stdlib. if any(path.is_relative_to(p) for p in SITE_PACKAGES_DIRS): return cls.SITE_PACKAGES if any(path.is_relative_to(p) for p in STDLIB_DIRS): return cls.STDLIB return cls.LOCAL # show local files first, then site-packages, then stdlib def _sort_key(path: str, lineno: int) -> tuple[int, str, int]: return (ModuleLocation.from_path(path), path, lineno) def make_report(explanations, *, cap_lines_at=5): report = defaultdict(list) for origin, locations in explanations.items(): locations = list(locations) locations.sort(key=lambda v: _sort_key(v[0], v[1])) report_lines = [f" {fname}:{lineno}" for fname, lineno in locations] if len(report_lines) > cap_lines_at + 1: msg = " (and {} more with settings.verbosity >= verbose)" report_lines[cap_lines_at:] = [msg.format(len(report_lines[cap_lines_at:]))] if report_lines: # We might have filtered out every location as uninformative. report[origin] = list(EXPLANATION_STUB) + report_lines return report def explanatory_lines(traces, settings): if Phase.explain in settings.phases and sys.gettrace() and not traces: return defaultdict(list) # Return human-readable report lines summarising the traces explanations = get_explaining_locations(traces) max_lines = 5 if settings.verbosity <= Verbosity.normal else float("inf") return make_report(explanations, cap_lines_at=max_lines) # beware the code below; we're using some heuristics to make a nicer report... @functools.lru_cache def _get_git_repo_root() -> Path: try: where = subprocess.run( ["git", "rev-parse", "--show-toplevel"], check=True, timeout=10, capture_output=True, text=True, encoding="utf-8", ).stdout.strip() except Exception: # pragma: no cover return Path().absolute().parents[-1] else: return Path(where) def tractable_coverage_report(trace: Trace) -> dict[str, list[int]]: """Report a simple coverage map which is (probably most) of the user's code.""" coverage: dict = {} t = dict(trace) for file, line in set(t.keys()).union(t.values()) - {None}: # type: ignore # On Python <= 3.11, we can use coverage.py xor Hypothesis' tracer, # so the trace will be empty and this line never run under coverage. coverage.setdefault(file, set()).add(line) # pragma: no cover stdlib_fragment = f"{os.sep}lib{os.sep}python3.{sys.version_info.minor}{os.sep}" return { k: sorted(v) for k, v in coverage.items() if stdlib_fragment not in k and (p := Path(k)).is_relative_to(_get_git_repo_root()) and "site-packages" not in p.parts } ================================================ FILE: hypothesis-python/src/hypothesis/internal/validation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import decimal import math from numbers import Rational, Real from hypothesis.errors import InvalidArgument from hypothesis.internal.coverage import check_function @check_function def check_type(typ: type | tuple[type, ...], arg: object, name: str) -> None: if not isinstance(arg, typ): if isinstance(typ, tuple): assert len(typ) >= 2, "Use bare type instead of len-1 tuple" typ_string = "one of " + ", ".join(t.__name__ for t in typ) else: typ_string = typ.__name__ if typ_string == "SearchStrategy": from hypothesis.strategies import SearchStrategy # Use hypothesis.strategies._internal.strategies.check_strategy # instead, as it has some helpful "did you mean..." logic. assert typ is not SearchStrategy, "use check_strategy instead" raise InvalidArgument( f"Expected {typ_string} but got {name}={arg!r} (type={type(arg).__name__})" ) @check_function def check_valid_integer(value, name): """Checks that value is either unspecified, or a valid integer. Otherwise raises InvalidArgument. """ if value is None: return check_type(int, value, name) @check_function def check_valid_bound(value, name): """Checks that value is either unspecified, or a valid interval bound. Otherwise raises InvalidArgument. """ if value is None or isinstance(value, (int, Rational)): return if not isinstance(value, (Real, decimal.Decimal)): raise InvalidArgument(f"{name}={value!r} must be a real number.") if math.isnan(value): raise InvalidArgument(f"Invalid end point {name}={value!r}") @check_function def check_valid_magnitude(value, name): """Checks that value is either unspecified, or a non-negative valid interval bound. Otherwise raises InvalidArgument. """ check_valid_bound(value, name) if value is not None and value < 0: raise InvalidArgument(f"{name}={value!r} must not be negative.") elif value is None and name == "min_magnitude": raise InvalidArgument("Use min_magnitude=0 or omit the argument entirely.") @check_function def try_convert(typ, value, name): if value is None: return None if isinstance(value, typ): return value try: return typ(value) except (TypeError, ValueError, ArithmeticError) as err: raise InvalidArgument( f"Cannot convert {name}={value!r} of type " f"{type(value).__name__} to type {typ.__name__}" ) from err @check_function def check_valid_size(value, name): """Checks that value is either unspecified, or a valid non-negative size expressed as an integer. Otherwise raises InvalidArgument. """ if value is None and name not in ("min_size", "size"): return check_type(int, value, name) if value < 0: raise InvalidArgument(f"Invalid size {name}={value!r} < 0") @check_function def check_valid_interval(lower_bound, upper_bound, lower_name, upper_name): """Checks that lower_bound and upper_bound are either unspecified, or they define a valid interval on the number line. Otherwise raises InvalidArgument. """ if lower_bound is None or upper_bound is None: return if upper_bound < lower_bound: raise InvalidArgument( f"Cannot have {upper_name}={upper_bound!r} < {lower_name}={lower_bound!r}" ) @check_function def check_valid_sizes(min_size, max_size): check_valid_size(min_size, "min_size") check_valid_size(max_size, "max_size") check_valid_interval(min_size, max_size, "min_size", "max_size") ================================================ FILE: hypothesis-python/src/hypothesis/provisional.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This module contains various provisional APIs and strategies. It is intended for internal use, to ease code reuse, and is not stable. Point releases may move or break the contents at any time! Internet strategies should conform to :rfc:`3986` or the authoritative definitions it links to. If not, report the bug! """ # https://tools.ietf.org/html/rfc3696 import string from functools import lru_cache from importlib import resources from hypothesis import strategies as st from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.strategies import DrawFn from hypothesis.strategies._internal.utils import defines_strategy URL_SAFE_CHARACTERS = frozenset(string.ascii_letters + string.digits + "$-_.+!*'(),~") FRAGMENT_SAFE_CHARACTERS = URL_SAFE_CHARACTERS | {"?", "/"} @lru_cache(maxsize=1) def get_top_level_domains() -> tuple[str, ...]: # This file is sourced from http://data.iana.org/TLD/tlds-alpha-by-domain.txt # The file contains additional information about the date that it was last updated. traversable = resources.files("hypothesis.vendor") / "tlds-alpha-by-domain.txt" _comment, *_tlds = traversable.read_text(encoding="utf-8").splitlines() assert _comment.startswith("#") # Remove special-use domain names from the list. For more discussion # see https://github.com/HypothesisWorks/hypothesis/pull/3572 return ("COM", *sorted((d for d in _tlds if d != "ARPA"), key=len)) @st.composite def _recase_randomly(draw: DrawFn, tld: str) -> str: tld = list(tld) changes = draw(st.tuples(*(st.booleans() for _ in range(len(tld))))) for i, change_case in enumerate(changes): if change_case: tld[i] = tld[i].lower() if tld[i].isupper() else tld[i].upper() return "".join(tld) class DomainNameStrategy(st.SearchStrategy[str]): @staticmethod def clean_inputs( minimum: int, maximum: int, value: int | None, variable_name: str ) -> int: if value is None: value = maximum elif not isinstance(value, int): raise InvalidArgument( f"Expected integer but {variable_name} is a {type(value).__name__}" ) elif not minimum <= value <= maximum: raise InvalidArgument( f"Invalid value {minimum!r} < {variable_name}={value!r} < {maximum!r}" ) return value def __init__( self, max_length: int | None = None, max_element_length: int | None = None ) -> None: """ A strategy for :rfc:`1035` fully qualified domain names. The upper limit for max_length is 255 in accordance with :rfc:`1035#section-2.3.4` The lower limit for max_length is 4, corresponding to a two letter domain with a single letter subdomain. The upper limit for max_element_length is 63 in accordance with :rfc:`1035#section-2.3.4` The lower limit for max_element_length is 1 in accordance with :rfc:`1035#section-2.3.4` """ # https://tools.ietf.org/html/rfc1035#section-2.3.4 max_length = self.clean_inputs(4, 255, max_length, "max_length") max_element_length = self.clean_inputs( 1, 63, max_element_length, "max_element_length" ) super().__init__() self.max_length = max_length self.max_element_length = max_element_length # These regular expressions are constructed to match the documented # information in https://tools.ietf.org/html/rfc1035#section-2.3.1 # which defines the allowed syntax of a subdomain string. if self.max_element_length == 1: label_regex = r"[a-zA-Z]" elif self.max_element_length == 2: label_regex = r"[a-zA-Z][a-zA-Z0-9]?" else: maximum_center_character_pattern_repetitions = self.max_element_length - 2 label_regex = r"[a-zA-Z]([a-zA-Z0-9\-]{0,%d}[a-zA-Z0-9])?" % ( maximum_center_character_pattern_repetitions, ) # Construct reusable strategies here to avoid a performance hit by doing # so repeatedly in do_draw. # 1 - Select a valid top-level domain (TLD) name # 2 - Check that the number of characters in our selected TLD won't # prevent us from generating at least a 1 character subdomain. # 3 - Randomize the TLD between upper and lower case characters. self.domain_strategy = ( st.sampled_from(get_top_level_domains()) .filter(lambda tld: len(tld) + 2 <= self.max_length) .flatmap(_recase_randomly) ) # RFC-5890 s2.3.1 says such labels are reserved, and since we don't # want to bother with xn-- punycode labels we'll exclude them all. self.elem_strategy = st.from_regex(label_regex, fullmatch=True).filter( lambda label: len(label) < 4 or label[2:4] != "--" ) def do_draw(self, data: ConjectureData) -> str: domain = data.draw(self.domain_strategy) # The maximum possible number of subdomains is 126, # 1 character subdomain + 1 '.' character, * 126 = 252, # with a max of 255, that leaves 3 characters for a TLD. # Allowing any more subdomains would not leave enough # characters for even the shortest possible TLDs. elements = cu.many(data, min_size=1, average_size=3, max_size=126) while elements.more(): # Generate a new valid subdomain using the regex strategy. sub_domain = data.draw(self.elem_strategy) if len(domain) + len(sub_domain) >= self.max_length: data.stop_span(discard=True) break domain = sub_domain + "." + domain return domain @defines_strategy(force_reusable_values=True) def domains( *, max_length: int = 255, max_element_length: int = 63 ) -> st.SearchStrategy[str]: """Generate :rfc:`1035` compliant fully qualified domain names.""" return DomainNameStrategy( max_length=max_length, max_element_length=max_element_length ) # The `urls()` strategy uses this to generate URL fragments (e.g. "#foo"). # It has been extracted to top-level so that we can test it independently # of `urls()`, which helps with getting non-flaky coverage of the lambda. _url_fragments_strategy = ( st.lists( st.builds( lambda char, encode: ( f"%{ord(char):02X}" if (encode or char not in FRAGMENT_SAFE_CHARACTERS) else char ), st.characters(min_codepoint=0, max_codepoint=255), st.booleans(), ), min_size=1, ) .map("".join) .map("#{}".format) ) @defines_strategy(force_reusable_values=True) def urls() -> st.SearchStrategy[str]: """A strategy for :rfc:`3986`, generating http/https URLs. The generated URLs could, at least in theory, be passed to an HTTP client and fetched. """ def url_encode(s: str) -> str: return "".join(c if c in URL_SAFE_CHARACTERS else f"%{ord(c):02X}" for c in s) schemes = st.sampled_from(["http", "https"]) ports = st.integers(min_value=1, max_value=2**16 - 1).map(":{}".format) paths = st.lists(st.text(string.printable).map(url_encode)).map("/".join) return st.builds( "{}://{}{}/{}{}".format, schemes, domains(), st.just("") | ports, paths, st.just("") | _url_fragments_strategy, ) ================================================ FILE: hypothesis-python/src/hypothesis/py.typed ================================================ ================================================ FILE: hypothesis-python/src/hypothesis/reporting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Callable from contextlib import AbstractContextManager from typing import TypeAlias from hypothesis._settings import Verbosity, settings from hypothesis.internal.compat import escape_unicode_characters from hypothesis.utils.dynamicvariables import DynamicVariable def default(value: object) -> None: try: print(value) except UnicodeEncodeError: print(escape_unicode_characters(str(value))) ReporterT: TypeAlias = Callable[[object], None] reporter = DynamicVariable[ReporterT](default) def current_reporter() -> ReporterT: return reporter.value def with_reporter(new_reporter: ReporterT) -> AbstractContextManager[None]: return reporter.with_value(new_reporter) def current_verbosity() -> Verbosity: assert settings.default is not None return settings.default.verbosity def verbose_report(text: str) -> None: if current_verbosity() >= Verbosity.verbose: base_report(text) def debug_report(text: str) -> None: if current_verbosity() >= Verbosity.debug: base_report(text) def report(text: str) -> None: if current_verbosity() >= Verbosity.normal: base_report(text) def base_report(text: str) -> None: assert isinstance(text, str), f"unexpected non-str {text=}" current_reporter()(text) ================================================ FILE: hypothesis-python/src/hypothesis/stateful.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This module provides support for a stateful style of testing, where tests attempt to find a sequence of operations that cause a breakage rather than just a single value. Notably, the set of steps available at any point may depend on the execution to date. """ import collections import dataclasses import inspect from collections.abc import Callable, Iterable, Sequence from dataclasses import dataclass, field from functools import lru_cache from io import StringIO from time import perf_counter from typing import Any, ClassVar, TypeVar, overload from unittest import TestCase from hypothesis import strategies as st from hypothesis._settings import HealthCheck, Verbosity, settings as Settings from hypothesis.control import _current_build_context, current_build_context from hypothesis.core import TestFunc, given from hypothesis.errors import ( FlakyStrategyDefinition, InvalidArgument, InvalidDefinition, ) from hypothesis.internal.compat import add_note, batched from hypothesis.internal.conjecture.engine import BUFFER_SIZE from hypothesis.internal.conjecture.junkdrawer import gc_cumulative_time from hypothesis.internal.conjecture.utils import calc_label_from_name from hypothesis.internal.healthcheck import fail_health_check from hypothesis.internal.observability import observability_enabled from hypothesis.internal.reflection import ( function_digest, get_pretty_function_description, nicerepr, proxies, ) from hypothesis.internal.validation import check_type from hypothesis.reporting import current_verbosity, report from hypothesis.strategies._internal.featureflags import FeatureStrategy from hypothesis.strategies._internal.strategies import ( Ex, OneOfStrategy, SearchStrategy, check_strategy, ) from hypothesis.utils.deprecation import note_deprecation from hypothesis.vendor.pretty import RepresentationPrinter T = TypeVar("T") STATE_MACHINE_RUN_LABEL = calc_label_from_name("another state machine step") def _is_singleton(obj: object) -> bool: """ Returns True if two separately created instances of v will have the same id (due to interning). """ # The range [-5, 256] is a cpython implementation detail. This may not work # well on other platforms. if isinstance(obj, int) and -5 <= obj <= 256: return True # cpython also interns compile-time strings, but let's just ignore those for # now. return isinstance(obj, bool) or obj is None class _OmittedArgument: """Sentinel class to prevent overlapping overloads in type hints. See comments above the overloads of @rule.""" class TestCaseProperty: # pragma: no cover def __get__(self, obj, typ=None): if obj is not None: typ = type(obj) return typ._to_test_case() def __set__(self, obj, value): raise AttributeError("Cannot set TestCase") def __delete__(self, obj): raise AttributeError("Cannot delete TestCase") def get_state_machine_test( state_machine_factory, *, settings=None, _min_steps=0, _flaky_state=None ): # This function is split out from run_state_machine_as_test so that # HypoFuzz can get and call the test function directly. if settings is None: try: settings = state_machine_factory.TestCase.settings check_type(Settings, settings, "state_machine_factory.TestCase.settings") except AttributeError: settings = Settings(deadline=None, suppress_health_check=list(HealthCheck)) check_type(Settings, settings, "settings") check_type(int, _min_steps, "_min_steps") if _min_steps < 0: # Because settings can vary via e.g. profiles, settings.stateful_step_count # overrides this argument and we don't bother cross-validating. raise InvalidArgument(f"_min_steps={_min_steps} must be non-negative.") _flaky_state = _flaky_state or {} @settings @given(st.data()) def run_state_machine(data): cd = data.conjecture_data machine: RuleBasedStateMachine = state_machine_factory() check_type(RuleBasedStateMachine, machine, "state_machine_factory()") cd.hypothesis_runner = machine machine._observability_predicates = cd._observability_predicates # alias print_steps = ( current_build_context().is_final or current_verbosity() >= Verbosity.debug ) cd._stateful_repr_parts = [] def output(s): if print_steps: report(s) if observability_enabled(): cd._stateful_repr_parts.append(s) try: output(f"state = {machine.__class__.__name__}()") machine.check_invariants(settings, output, cd._stateful_run_times) max_steps = settings.stateful_step_count steps_run = 0 while True: # We basically always want to run the maximum number of steps, # but need to leave a small probability of terminating early # in order to allow for reducing the number of steps once we # find a failing test case, so we stop with probability of # 2 ** -16 during normal operation but force a stop when we've # generated enough steps. cd.start_span(STATE_MACHINE_RUN_LABEL) must_stop = None if steps_run >= max_steps: must_stop = True elif steps_run <= _min_steps: must_stop = False elif cd.length > (0.8 * BUFFER_SIZE): # Better to stop after fewer steps, than always overrun and retry. # See https://github.com/HypothesisWorks/hypothesis/issues/3618 must_stop = True start_draw = perf_counter() start_gc = gc_cumulative_time() if cd.draw_boolean(p=2**-16, forced=must_stop): break steps_run += 1 # Choose a rule to run, preferring an initialize rule if there are # any which have not been run yet. _flaky_state["selecting_rule"] = True if machine._initialize_rules_to_run: init_rules = [ st.tuples(st.just(rule), st.fixed_dictionaries(rule.arguments)) for rule in machine._initialize_rules_to_run ] rule, data = cd.draw(st.one_of(init_rules)) machine._initialize_rules_to_run.remove(rule) else: rule, data = cd.draw(machine._rules_strategy) _flaky_state["selecting_rule"] = False draw_label = f"generate:rule:{rule.function.__name__}" cd.draw_times.setdefault(draw_label, 0.0) in_gctime = gc_cumulative_time() - start_gc cd.draw_times[draw_label] += perf_counter() - start_draw - in_gctime # Pretty-print the values this rule was called with *before* calling # _add_results_to_targets, to avoid printing arguments which are also # a return value using the variable name they are assigned to. # See https://github.com/HypothesisWorks/hypothesis/issues/2341 if print_steps or observability_enabled(): data_to_print = { k: machine._pretty_print(v) for k, v in data.items() } # Assign 'result' here in case executing the rule fails below result = multiple() try: data = dict(data) for k, v in list(data.items()): if isinstance(v, VarReference): data[k] = machine.names_to_values[v.name] elif isinstance(v, list) and all( isinstance(item, VarReference) for item in v ): data[k] = [machine.names_to_values[item.name] for item in v] label = f"execute:rule:{rule.function.__name__}" start = perf_counter() start_gc = gc_cumulative_time() result = rule.function(machine, **data) in_gctime = gc_cumulative_time() - start_gc cd._stateful_run_times[label] += perf_counter() - start - in_gctime if rule.targets: if isinstance(result, MultipleResults): machine._add_results_to_targets(rule.targets, result.values) else: machine._add_results_to_targets(rule.targets, [result]) elif result is not None: fail_health_check( settings, "Rules should return None if they have no target bundle, " f"but {rule.function.__qualname__} returned {result!r}", HealthCheck.return_value, ) finally: if print_steps or observability_enabled(): # 'result' is only used if the step has target bundles. # If it does, and the result is a 'MultipleResult', # then 'print_step' prints a multi-variable assignment. output(machine._repr_step(rule, data_to_print, result)) machine.check_invariants(settings, output, cd._stateful_run_times) cd.stop_span() finally: output("state.teardown()") machine.teardown() # Use a machine digest to identify stateful tests in the example database run_state_machine.hypothesis.inner_test._hypothesis_internal_add_digest = ( function_digest(state_machine_factory) ) # Copy some attributes so @seed and @reproduce_failure "just work" run_state_machine._hypothesis_internal_use_seed = getattr( state_machine_factory, "_hypothesis_internal_use_seed", None ) run_state_machine._hypothesis_internal_use_reproduce_failure = getattr( state_machine_factory, "_hypothesis_internal_use_reproduce_failure", None ) run_state_machine._hypothesis_internal_print_given_args = False return run_state_machine def run_state_machine_as_test(state_machine_factory, *, settings=None, _min_steps=0): """Run a state machine definition as a test, either silently doing nothing or printing a minimal breaking program and raising an exception. state_machine_factory is anything which returns an instance of RuleBasedStateMachine when called with no arguments - it can be a class or a function. settings will be used to control the execution of the test. """ flaky_state = {"selecting_rule": False} state_machine_test = get_state_machine_test( state_machine_factory, settings=settings, _min_steps=_min_steps, _flaky_state=flaky_state, ) try: state_machine_test() except FlakyStrategyDefinition as err: if flaky_state["selecting_rule"]: add_note( err, "while selecting a rule to run. This is usually caused by " "a flaky precondition, or a bundle that was unexpectedly empty.", ) raise class StateMachineMeta(type): def __setattr__(cls, name, value): if name == "settings" and isinstance(value, Settings): descr = f"settings({value.show_changed()})" raise AttributeError( f"Assigning {cls.__name__}.settings = {descr} does nothing. Assign " f"to {cls.__name__}.TestCase.settings, or use @{descr} as a decorator " f"on the {cls.__name__} class." ) return super().__setattr__(name, value) @dataclass(slots=True, frozen=True) class _SetupState: rules: list["Rule"] invariants: list["Invariant"] initializers: list["Rule"] class RuleBasedStateMachine(metaclass=StateMachineMeta): """A RuleBasedStateMachine gives you a structured way to define state machines. The idea is that a state machine carries the system under test and some supporting data. This data can be stored in instance variables or divided into Bundles. The state machine has a set of rules which may read data from bundles (or just from normal strategies), push data onto bundles, change the state of the machine, or verify properties. At any given point a random applicable rule will be executed. """ _setup_state_per_class: ClassVar[dict[type, _SetupState]] = {} def __init__(self) -> None: setup_state = self.setup_state() if not setup_state.rules: raise InvalidDefinition( f"State machine {type(self).__name__} defines no rules" ) if isinstance(s := vars(type(self)).get("settings"), Settings): tname = type(self).__name__ descr = f"settings({s.show_changed()})" raise InvalidDefinition( f"Assigning settings = {descr} as a class attribute does nothing. " f"Assign to {tname}.TestCase.settings, or use @{descr} as a decorator " f"on the {tname} class." ) self.rules = setup_state.rules self.invariants = setup_state.invariants # copy since we pop from this as we run initialize rules. self._initialize_rules_to_run = setup_state.initializers.copy() self.bundles: dict[str, list] = {} self.names_counters: collections.Counter = collections.Counter() self.names_list: list[str] = [] self.names_to_values: dict[str, Any] = {} self.__stream = StringIO() self.__printer = RepresentationPrinter( self.__stream, context=_current_build_context.value ) self._rules_strategy = RuleStrategy(self) def _pretty_print(self, value): if isinstance(value, VarReference): return value.name elif isinstance(value, list) and all( isinstance(item, VarReference) for item in value ): return "[" + ", ".join([item.name for item in value]) + "]" self.__stream.seek(0) self.__stream.truncate(0) self.__printer.output_width = 0 self.__printer.buffer_width = 0 self.__printer.buffer.clear() self.__printer.pretty(value) self.__printer.flush() return self.__stream.getvalue() def __repr__(self): return f"{type(self).__name__}({nicerepr(self.bundles)})" def _new_name(self, target): result = f"{target}_{self.names_counters[target]}" self.names_counters[target] += 1 self.names_list.append(result) return result def _last_names(self, n: int) -> list[str]: len_ = len(self.names_list) assert len_ >= n return self.names_list[len_ - n :] def bundle(self, name): return self.bundles.setdefault(name, []) @classmethod def setup_state(cls): try: return cls._setup_state_per_class[cls] except KeyError: pass rules: list[Rule] = [] initializers: list[Rule] = [] invariants: list[Invariant] = [] for _name, f in inspect.getmembers(cls): rule = getattr(f, RULE_MARKER, None) initializer = getattr(f, INITIALIZE_RULE_MARKER, None) invariant = getattr(f, INVARIANT_MARKER, None) if rule is not None: rules.append(rule) if initializer is not None: initializers.append(initializer) if invariant is not None: invariants.append(invariant) if ( getattr(f, PRECONDITIONS_MARKER, None) is not None and rule is None and invariant is None ): raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with @precondition, " "but not @rule (or @invariant), which is not allowed. A " "precondition must be combined with a rule or an invariant, " "since it has no effect alone." ) state = _SetupState( rules=rules, initializers=initializers, invariants=invariants ) cls._setup_state_per_class[cls] = state return state def _repr_step(self, rule: "Rule", data: Any, result: Any) -> str: output_assignment = "" extra_assignment_lines = [] if rule.targets: number_of_results = ( len(result.values) if isinstance(result, MultipleResults) else 1 ) number_of_last_names = len(rule.targets) * number_of_results last_names = self._last_names(number_of_last_names) if isinstance(result, MultipleResults): if len(result.values) == 1: # len-1 tuples output_per_target = [f"({name},)" for name in last_names] output_assignment = " = ".join(output_per_target) + " = " elif result.values: # multiple values, multiple targets -- use the first target # for the assignment from function, and do the other target # assignments on separate lines names_per_target = list(batched(last_names, number_of_results)) first_target_output = ", ".join(names_per_target[0]) output_assignment = first_target_output + " = " for other_target_names in names_per_target[1:]: other_target_output = ", ".join(other_target_names) extra_assignment_lines.append( other_target_output + " = " + first_target_output ) else: output_assignment = " = ".join(last_names) + " = " args = ", ".join(f"{k}={v}" for k, v in data.items()) output_line = f"{output_assignment}state.{rule.function.__name__}({args})" return "\n".join([output_line] + extra_assignment_lines) def _add_results_to_targets(self, targets, results): # Note, the assignment order here is reflected in _repr_step for target in targets: for result in results: name = self._new_name(target) def printer(obj, p, cycle, name=name): return p.text(name) # see # https://github.com/HypothesisWorks/hypothesis/pull/4266#discussion_r1949619102 if not _is_singleton(result): self.__printer.singleton_pprinters.setdefault(id(result), printer) self.names_to_values[name] = result self.bundles.setdefault(target, []).append(VarReference(name)) def check_invariants(self, settings, output, runtimes): for invar in self.invariants: if self._initialize_rules_to_run and not invar.check_during_init: continue if not all(precond(self) for precond in invar.preconditions): continue name = invar.function.__name__ if ( current_build_context().is_final or settings.verbosity >= Verbosity.debug or observability_enabled() ): output(f"state.{name}()") start = perf_counter() result = invar.function(self) runtimes[f"execute:invariant:{name}"] += perf_counter() - start if result is not None: fail_health_check( settings, "The return value of an @invariant is always ignored, but " f"{invar.function.__qualname__} returned {result!r} " "instead of None", HealthCheck.return_value, ) def teardown(self): """Called after a run has finished executing to clean up any necessary state. Does nothing by default. """ TestCase = TestCaseProperty() @classmethod @lru_cache def _to_test_case(cls): class StateMachineTestCase(TestCase): settings = Settings(deadline=None, suppress_health_check=list(HealthCheck)) def runTest(self): run_state_machine_as_test(cls, settings=self.settings) runTest.is_hypothesis_test = True runTest._hypothesis_state_machine_class = cls StateMachineTestCase.__name__ = cls.__name__ + ".TestCase" StateMachineTestCase.__qualname__ = cls.__qualname__ + ".TestCase" return StateMachineTestCase @dataclass(slots=True, frozen=False) class Rule: targets: Any function: Any arguments: Any preconditions: Any bundles: tuple["Bundle", ...] = field(init=False) _cached_hash: int | None = field(init=False, default=None) _cached_repr: str | None = field(init=False, default=None) arguments_strategies: dict[Any, Any] = field(init=False, default_factory=dict) def __post_init__(self): bundles = [] for k, v in sorted(self.arguments.items()): assert not isinstance(v, BundleReferenceStrategy) if isinstance(v, Bundle): bundles.append(v) consume = isinstance(v, BundleConsumer) v = BundleReferenceStrategy(v.name, consume=consume) self.arguments_strategies[k] = v self.bundles = tuple(bundles) def __repr__(self) -> str: if self._cached_repr is None: bits = [ f"{field.name}=" f"{get_pretty_function_description(getattr(self, field.name))}" for field in dataclasses.fields(self) if getattr(self, field.name) ] self._cached_repr = f"{self.__class__.__name__}({', '.join(bits)})" return self._cached_repr def __hash__(self): # sampled_from uses hash in calc_label, and we want this to be fast when # sampling stateful rules, so we cache here. if self._cached_hash is None: self._cached_hash = hash( ( self.targets, self.function, tuple(self.arguments.items()), self.preconditions, self.bundles, ) ) return self._cached_hash self_strategy = st.runner() class BundleReferenceStrategy(SearchStrategy): def __init__(self, name: str, *, consume: bool = False): super().__init__() self.name = name self.consume = consume def do_draw(self, data): machine = data.draw(self_strategy) bundle = machine.bundle(self.name) if not bundle: data.mark_invalid(f"Cannot draw from empty bundle {self.name!r}") # Shrink towards the right rather than the left. This makes it easier # to delete data generated earlier, as when the error is towards the # end there can be a lot of hard to remove padding. position = data.draw_integer(0, len(bundle) - 1, shrink_towards=len(bundle)) if self.consume: return bundle.pop(position) # pragma: no cover # coverage is flaky here else: return bundle[position] class Bundle(SearchStrategy[Ex]): """A collection of values for use in stateful testing. Bundles are a kind of strategy where values can be added by rules, and (like any strategy) used as inputs to future rules. The ``name`` argument they are passed is the they are referred to internally by the state machine; no two bundles may have the same name. It is idiomatic to use the attribute being assigned to as the name of the Bundle:: class MyStateMachine(RuleBasedStateMachine): keys = Bundle("keys") Bundles can contain the same value more than once; this becomes relevant when using :func:`~hypothesis.stateful.consumes` to remove values again. If the ``consume`` argument is set to True, then all values that are drawn from this bundle will be consumed (as above) when requested. """ def __init__( self, name: str, *, consume: bool = False, draw_references: bool = True ) -> None: super().__init__() self.name = name self.__reference_strategy = BundleReferenceStrategy(name, consume=consume) self.draw_references = draw_references def do_draw(self, data): machine = data.draw(self_strategy) reference = data.draw(self.__reference_strategy) return machine.names_to_values[reference.name] def __repr__(self): consume = self.__reference_strategy.consume if consume is False: return f"Bundle(name={self.name!r})" return f"Bundle(name={self.name!r}, {consume=})" def calc_is_empty(self, recur): # We assume that a bundle will grow over time return False def is_currently_empty(self, data): # ``self_strategy`` is an instance of the ``st.runner()`` strategy. # Hence drawing from it only returns the current state machine without # modifying the underlying choice sequence. machine = data.draw(self_strategy) return not bool(machine.bundle(self.name)) def flatmap(self, expand): if self.draw_references: return type(self)( self.name, consume=self.__reference_strategy.consume, draw_references=False, ).flatmap(expand) return super().flatmap(expand) def __hash__(self): # Making this hashable means we hit the fast path of "everything is # hashable" in st.sampled_from label calculation when sampling which rule # to invoke next. # Mix in "Bundle" for collision resistance return hash(("Bundle", self.name)) class BundleConsumer(Bundle[Ex]): def __init__(self, bundle: Bundle[Ex]) -> None: super().__init__(bundle.name, consume=True) def consumes(bundle: Bundle[Ex]) -> SearchStrategy[Ex]: """When introducing a rule in a RuleBasedStateMachine, this function can be used to mark bundles from which each value used in a step with the given rule should be removed. This function returns a strategy object that can be manipulated and combined like any other. For example, a rule declared with ``@rule(value1=b1, value2=consumes(b2), value3=lists(consumes(b3)))`` will consume a value from Bundle ``b2`` and several values from Bundle ``b3`` to populate ``value2`` and ``value3`` each time it is executed. """ if not isinstance(bundle, Bundle): raise TypeError("Argument to be consumed must be a bundle.") return BundleConsumer(bundle) @dataclass(slots=True, frozen=True) class MultipleResults(Iterable[Ex]): values: tuple[Ex, ...] def __iter__(self): return iter(self.values) def multiple(*args: T) -> MultipleResults[T]: """This function can be used to pass multiple results to the target(s) of a rule. Just use ``return multiple(result1, result2, ...)`` in your rule. It is also possible to use ``return multiple()`` with no arguments in order to end a rule without passing any result. """ return MultipleResults(args) def _convert_targets(targets, target): """Single validator and converter for target arguments.""" if target is not None: if targets: raise InvalidArgument( f"Passing both targets={targets!r} and target={target!r} is " f"redundant - pass targets={(*targets, target)!r} instead." ) targets = (target,) converted_targets = [] for t in targets: if not isinstance(t, Bundle): msg = "Got invalid target %r of type %r, but all targets must be Bundles." if isinstance(t, OneOfStrategy): msg += ( "\nIt looks like you passed `one_of(a, b)` or `a | b` as " "a target. You should instead pass `targets=(a, b)` to " "add the return value of this rule to both the `a` and " "`b` bundles, or define a rule for each target if it " "should be added to exactly one." ) raise InvalidArgument(msg % (t, type(t))) while isinstance(t, Bundle): if isinstance(t, BundleConsumer): note_deprecation( f"Using consumes({t.name}) doesn't makes sense in this context. " "This will be an error in a future version of Hypothesis.", since="2021-09-08", has_codemod=False, stacklevel=2, ) t = t.name converted_targets.append(t) return tuple(converted_targets) RULE_MARKER = "hypothesis_stateful_rule" INITIALIZE_RULE_MARKER = "hypothesis_stateful_initialize_rule" PRECONDITIONS_MARKER = "hypothesis_stateful_preconditions" INVARIANT_MARKER = "hypothesis_stateful_invariant" _RuleType = Callable[..., MultipleResults[Ex] | Ex] _RuleWrapper = Callable[[_RuleType[Ex]], _RuleType[Ex]] def _rule_qualname(f: Any) -> str: # we define rules / invariants / initializes inside of wrapper functions, which # makes f.__qualname__ look like: # test_precondition..BadStateMachine.has_precondition_but_no_rule # which is not ideal. This function returns just # BadStateMachine.has_precondition_but_no_rule # instead. return f.__qualname__.rsplit(".")[-1] # We cannot exclude `target` or `targets` from any of these signatures because # otherwise they would be matched against the `kwargs`, either leading to # overlapping overloads of incompatible return types, or a concrete # implementation that does not accept all overloaded variant signatures. # Although it is possible to reorder the variants to fix the former, it will # always lead to the latter, as then the omitted parameter could be typed as # a `SearchStrategy`, which the concrete implementation does not accept. # # Omitted `targets` parameters, where the default value is used, are typed with # a special `_OmittedArgument` type. We cannot type them as `tuple[()]`, because # `tuple[()]` is a subtype of `Sequence[Bundle[Ex]]`, leading to signature # overlaps with incompatible return types. The `_OmittedArgument` type will never be # encountered at runtime, and exists solely to annotate the default of `targets`. # PEP 661 (Sentinel Values) might provide a more elegant alternative in the future. # # We could've also annotated `targets` as `tuple[_OmittedArgument]`, but then when # both `target` and `targets` are provided, mypy describes the type error as an # invalid argument type for `targets` (expected `tuple[_OmittedArgument]`, got ...). # By annotating it as a bare `_OmittedArgument` type, mypy's error will warn that # there is no overloaded signature matching the call, which is more descriptive. # # When `target` xor `targets` is provided, the function to decorate must return # a value whose type matches the one stored in the bundle. When neither are # provided, the function to decorate must return nothing. There is no variant # for providing `target` and `targets`, as these parameters are mutually exclusive. @overload def rule( *, targets: Sequence[Bundle[Ex]], target: None = ..., **kwargs: SearchStrategy, ) -> _RuleWrapper[Ex]: # pragma: no cover ... @overload def rule( *, target: Bundle[Ex], targets: _OmittedArgument = ..., **kwargs: SearchStrategy ) -> _RuleWrapper[Ex]: # pragma: no cover ... @overload def rule( *, target: None = ..., targets: _OmittedArgument = ..., **kwargs: SearchStrategy, ) -> Callable[[Callable[..., None]], Callable[..., None]]: # pragma: no cover ... def rule( *, targets: Sequence[Bundle[Ex]] | _OmittedArgument = (), target: Bundle[Ex] | None = None, **kwargs: SearchStrategy, ) -> _RuleWrapper[Ex] | Callable[[Callable[..., None]], Callable[..., None]]: """Decorator for RuleBasedStateMachine. Any Bundle present in ``target`` or ``targets`` will define where the end result of this function should go. If both are empty then the end result will be discarded. ``target`` must be a Bundle, or if the result should be replicated to multiple bundles you can pass a tuple of them as the ``targets`` argument. It is invalid to use both arguments for a single rule. If the result should go to exactly one of several bundles, define a separate rule for each case. kwargs then define the arguments that will be passed to the function invocation. If their value is a Bundle, or if it is ``consumes(b)`` where ``b`` is a Bundle, then values that have previously been produced for that bundle will be provided. If ``consumes`` is used, the value will also be removed from the bundle. Any other kwargs should be strategies and values from them will be provided. """ converted_targets = _convert_targets(targets, target) for k, v in kwargs.items(): check_strategy(v, name=k) def accept(f): if getattr(f, INVARIANT_MARKER, None): raise InvalidDefinition( f"{_rule_qualname(f)} is used with both @rule and @invariant, " "which is not allowed. A function may be either a rule or an " "invariant, but not both." ) existing_rule = getattr(f, RULE_MARKER, None) existing_initialize_rule = getattr(f, INITIALIZE_RULE_MARKER, None) if existing_rule is not None: raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with @rule twice, which is " "not allowed." ) if existing_initialize_rule is not None: raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with both @rule and " "@initialize, which is not allowed." ) preconditions = getattr(f, PRECONDITIONS_MARKER, ()) rule = Rule( targets=converted_targets, arguments=kwargs, function=f, preconditions=preconditions, ) @proxies(f) def rule_wrapper(*args, **kwargs): return f(*args, **kwargs) setattr(rule_wrapper, RULE_MARKER, rule) return rule_wrapper return accept # See also comments of `rule`'s overloads. @overload def initialize( *, targets: Sequence[Bundle[Ex]], target: None = ..., **kwargs: SearchStrategy, ) -> _RuleWrapper[Ex]: # pragma: no cover ... @overload def initialize( *, target: Bundle[Ex], targets: _OmittedArgument = ..., **kwargs: SearchStrategy ) -> _RuleWrapper[Ex]: # pragma: no cover ... @overload def initialize( *, target: None = ..., targets: _OmittedArgument = ..., **kwargs: SearchStrategy, ) -> Callable[[Callable[..., None]], Callable[..., None]]: # pragma: no cover ... def initialize( *, targets: Sequence[Bundle[Ex]] | _OmittedArgument = (), target: Bundle[Ex] | None = None, **kwargs: SearchStrategy, ) -> _RuleWrapper[Ex] | Callable[[Callable[..., None]], Callable[..., None]]: """Decorator for RuleBasedStateMachine. An initialize decorator behaves like a rule, but all ``@initialize()`` decorated methods will be called before any ``@rule()`` decorated methods, in an arbitrary order. Each ``@initialize()`` method will be called exactly once per run, unless one raises an exception - after which only the ``.teardown()`` method will be run. ``@initialize()`` methods may not have preconditions. """ converted_targets = _convert_targets(targets, target) for k, v in kwargs.items(): check_strategy(v, name=k) def accept(f): if getattr(f, INVARIANT_MARKER, None): raise InvalidDefinition( f"{_rule_qualname(f)} is used with both @initialize and @invariant, " "which is not allowed. A function may be either an initialization " "rule or an invariant, but not both." ) existing_rule = getattr(f, RULE_MARKER, None) existing_initialize_rule = getattr(f, INITIALIZE_RULE_MARKER, None) if existing_rule is not None: raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with both @rule and " "@initialize, which is not allowed." ) if existing_initialize_rule is not None: raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with @initialize twice, " "which is not allowed." ) preconditions = getattr(f, PRECONDITIONS_MARKER, ()) if preconditions: raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with both @initialize and " "@precondition, which is not allowed. An initialization rule " "runs unconditionally and may not have a precondition." ) rule = Rule( targets=converted_targets, arguments=kwargs, function=f, preconditions=preconditions, ) @proxies(f) def rule_wrapper(*args, **kwargs): return f(*args, **kwargs) setattr(rule_wrapper, INITIALIZE_RULE_MARKER, rule) return rule_wrapper return accept @dataclass(slots=True, frozen=True) class VarReference: name: str # There are multiple alternatives for annotating the `precond` type, all of them # have drawbacks. See https://github.com/HypothesisWorks/hypothesis/pull/3068#issuecomment-906642371 def precondition(precond: Callable[[Any], bool]) -> Callable[[TestFunc], TestFunc]: """Decorator to apply a precondition for rules in a RuleBasedStateMachine. Specifies a precondition for a rule to be considered as a valid step in the state machine, which is more efficient than using :func:`~hypothesis.assume` within the rule. The ``precond`` function will be called with the instance of RuleBasedStateMachine and should return True or False. Usually it will need to look at attributes on that instance. For example:: class MyTestMachine(RuleBasedStateMachine): state = 1 @precondition(lambda self: self.state != 0) @rule(numerator=integers()) def divide_with(self, numerator): self.state = numerator / self.state If multiple preconditions are applied to a single rule, it is only considered a valid step when all of them return True. Preconditions may be applied to invariants as well as rules. """ def decorator(f): @proxies(f) def precondition_wrapper(*args, **kwargs): return f(*args, **kwargs) existing_initialize_rule = getattr(f, INITIALIZE_RULE_MARKER, None) if existing_initialize_rule is not None: raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with both @initialize and " "@precondition, which is not allowed. An initialization rule " "runs unconditionally and may not have a precondition." ) rule = getattr(f, RULE_MARKER, None) invariant = getattr(f, INVARIANT_MARKER, None) if rule is not None: assert invariant is None new_rule = dataclasses.replace( rule, preconditions=(*rule.preconditions, precond) ) setattr(precondition_wrapper, RULE_MARKER, new_rule) elif invariant is not None: assert rule is None new_invariant = dataclasses.replace( invariant, preconditions=(*invariant.preconditions, precond) ) setattr(precondition_wrapper, INVARIANT_MARKER, new_invariant) else: setattr( precondition_wrapper, PRECONDITIONS_MARKER, (*getattr(f, PRECONDITIONS_MARKER, ()), precond), ) return precondition_wrapper return decorator @dataclass(slots=True, frozen=True) class Invariant: function: Any preconditions: Any check_during_init: bool def __repr__(self) -> str: parts = [ f"function={get_pretty_function_description(self.function)}", f"{self.preconditions=}", f"{self.check_during_init=}", ] return f"Invariant({', '.join(parts)})" def invariant(*, check_during_init: bool = False) -> Callable[[TestFunc], TestFunc]: """Decorator to apply an invariant for rules in a RuleBasedStateMachine. The decorated function will be run after every rule and can raise an exception to indicate failed invariants. For example:: class MyTestMachine(RuleBasedStateMachine): state = 1 @invariant() def is_nonzero(self): assert self.state != 0 By default, invariants are only checked after all :func:`@initialize() ` rules have been run. Pass ``check_during_init=True`` for invariants which can also be checked during initialization. """ check_type(bool, check_during_init, "check_during_init") def accept(f): if getattr(f, RULE_MARKER, None) or getattr(f, INITIALIZE_RULE_MARKER, None): raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with both @invariant and " "@rule, which is not allowed." ) existing_invariant = getattr(f, INVARIANT_MARKER, None) if existing_invariant is not None: raise InvalidDefinition( f"{_rule_qualname(f)} has been decorated with @invariant twice, " "which is not allowed." ) preconditions = getattr(f, PRECONDITIONS_MARKER, ()) invar = Invariant( function=f, preconditions=preconditions, check_during_init=check_during_init, ) @proxies(f) def invariant_wrapper(*args, **kwargs): return f(*args, **kwargs) setattr(invariant_wrapper, INVARIANT_MARKER, invar) return invariant_wrapper return accept class RuleStrategy(SearchStrategy): def __init__(self, machine: RuleBasedStateMachine) -> None: super().__init__() self.machine = machine self.rules = machine.rules.copy() self.enabled_rules_strategy = st.shared( FeatureStrategy(at_least_one_of={r.function.__name__ for r in self.rules}), key=("enabled rules", machine), ) # The order is a bit arbitrary. Primarily we're trying to group rules # that write to the same location together, and to put rules with no # target first as they have less effect on the structure. We order from # fewer to more arguments on grounds that it will plausibly need less # data. This probably won't work especially well and we could be # smarter about it, but it's better than just doing it in definition # order. self.rules.sort( key=lambda rule: ( sorted(rule.targets), len(rule.arguments), rule.function.__name__, ) ) self.rules_strategy = st.sampled_from(self.rules) def __repr__(self): return f"{self.__class__.__name__}(machine={self.machine.__class__.__name__}({{...}}))" def do_draw(self, data): if not any(self.is_valid(rule) for rule in self.rules): rules = ", ".join([rule.function.__name__ for rule in self.rules]) msg = ( f"No progress can be made from state {self.machine!r}, because no " f"available rule had a True precondition. rules: {rules}" ) raise InvalidDefinition(msg) from None feature_flags = data.draw(self.enabled_rules_strategy) def rule_is_enabled(r): # Note: The order of the filters here is actually quite important, # because checking is_enabled makes choices, so increases the size of # the choice sequence. This means that if we are in a case where many # rules are invalid we would make a lot more choices if we ask if they # are enabled before we ask if they are valid, so our test cases would # be artificially large. return self.is_valid(r) and feature_flags.is_enabled(r.function.__name__) rule = data.draw(self.rules_strategy.filter(rule_is_enabled)) arguments = {} for k, strat in rule.arguments_strategies.items(): try: arguments[k] = data.draw(strat) except Exception as err: rname = rule.function.__name__ add_note(err, f"while generating {k!r} from {strat!r} for rule {rname}") raise return (rule, arguments) def is_valid(self, rule): for b in rule.bundles: if not self.machine.bundle(b.name): return False predicates = self.machine._observability_predicates desc = f"{self.machine.__class__.__qualname__}, rule {rule.function.__name__}," for pred in rule.preconditions: meets_precond = pred(self.machine) where = f"{desc} precondition {get_pretty_function_description(pred)}" predicates[where].update_count(condition=meets_precond) if not meets_precond: return False return True ================================================ FILE: hypothesis-python/src/hypothesis/statistics.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from collections import Counter from collections.abc import Iterable from typing import TYPE_CHECKING, cast from hypothesis._settings import Phase from hypothesis.utils.dynamicvariables import DynamicVariable if TYPE_CHECKING: from hypothesis.internal.conjecture.engine import PhaseStatistics, StatisticsDict collector = DynamicVariable(None) def note_statistics(stats_dict: "StatisticsDict") -> None: callback = collector.value if callback is not None: callback(stats_dict) def describe_targets(best_targets: dict[str, float]) -> list[str]: """Return a list of lines describing the results of `target`, if any.""" # These lines are included in the general statistics description below, # but also printed immediately below failing examples to alleviate the # "threshold problem" where shrinking can make severe bug look trivial. # See https://github.com/HypothesisWorks/hypothesis/issues/2180 if not best_targets: return [] elif len(best_targets) == 1: label, score = next(iter(best_targets.items())) return [f"Highest target score: {score:g} ({label=})"] else: lines = ["Highest target scores:"] for label, score in sorted(best_targets.items(), key=lambda x: x[::-1]): lines.append(f"{score:>16g} ({label=})") return lines def format_ms(times: Iterable[float]) -> str: """Format `times` into a string representing approximate milliseconds. `times` is a collection of durations in seconds. """ ordered = sorted(times) n = len(ordered) - 1 if n < 0 or any(math.isnan(t) for t in ordered): # pragma: no cover return "NaN ms" lower = int(ordered[math.floor(n * 0.05)] * 1000) upper = int(ordered[math.ceil(n * 0.95)] * 1000) if upper == 0: return "< 1ms" elif lower == upper: return f"~ {lower}ms" else: return f"~ {lower}-{upper} ms" def describe_statistics(stats_dict: "StatisticsDict") -> str: """Return a multi-line string describing the passed run statistics. `stats_dict` must be a dictionary of data in the format collected by `hypothesis.internal.conjecture.engine.ConjectureRunner.statistics`. We DO NOT promise that this format will be stable or supported over time, but do aim to make it reasonably useful for downstream users. It's also meant to support benchmarking for research purposes. This function is responsible for the report which is printed in the terminal for our pytest --hypothesis-show-statistics option. """ lines = [stats_dict["nodeid"] + ":\n"] if "nodeid" in stats_dict else [] prev_failures = 0 for phase in (p.name for p in list(Phase)[1:]): d = cast("PhaseStatistics", stats_dict.get(phase + "-phase", {})) # Basic information we report for every phase cases = d.get("test-cases", []) if not cases: continue statuses = Counter(t["status"] for t in cases) runtime_ms = format_ms(t["runtime"] for t in cases) drawtime_ms = format_ms(t["drawtime"] for t in cases) lines.append( f" - during {phase} phase ({d['duration-seconds']:.2f} seconds):\n" f" - Typical runtimes: {runtime_ms}, of which {drawtime_ms} in data generation\n" f" - {statuses['valid']} passing examples, {statuses['interesting']} " f"failing examples, {statuses['invalid'] + statuses['overrun']} invalid examples" ) # If we've found new distinct failures in this phase, report them distinct_failures = d["distinct-failures"] - prev_failures if distinct_failures: plural = distinct_failures > 1 lines.append( " - Found {}{} distinct error{} in this phase".format( distinct_failures, " more" * bool(prev_failures), "s" * plural ) ) prev_failures = d["distinct-failures"] # Report events during the generate phase, if there were any if phase == "generate": events = Counter(sum((t["events"] for t in cases), [])) if events: lines.append(" - Events:") lines += [ f" * {100 * v / len(cases):.2f}%, {k}" for k, v in sorted(events.items(), key=lambda x: (-x[1], x[0])) ] # Some additional details on the shrinking phase if phase == "shrink": lines.append( " - Tried {} shrinks of which {} were successful".format( len(cases), d["shrinks-successful"] ) ) lines.append("") target_lines = describe_targets(stats_dict.get("targets", {})) if target_lines: lines.append(" - " + target_lines[0]) lines.extend(" " + l for l in target_lines[1:]) lines.append(" - Stopped because " + stats_dict["stopped-because"]) return "\n".join(lines) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.strategies._internal import SearchStrategy from hypothesis.strategies._internal.collections import tuples from hypothesis.strategies._internal.core import ( DataObject, DrawFn, binary, booleans, builds, characters, complex_numbers, composite, data, decimals, deferred, dictionaries, emails, fixed_dictionaries, fractions, from_regex, from_type, frozensets, functions, iterables, lists, permutations, random_module, randoms, recursive, register_type_strategy, runner, sampled_from, sets, shared, slices, text, uuids, ) from hypothesis.strategies._internal.datetime import ( dates, datetimes, timedeltas, times, timezone_keys, timezones, ) from hypothesis.strategies._internal.ipaddress import ip_addresses from hypothesis.strategies._internal.misc import just, none, nothing from hypothesis.strategies._internal.numbers import floats, integers from hypothesis.strategies._internal.strategies import one_of from hypothesis.strategies._internal.utils import _all_strategies # The implementation of all of these lives in `_strategies.py`, but we # re-export them via this module to avoid exposing implementation details # to over-zealous tab completion in editors that do not respect __all__. __all__ = [ "DataObject", "DrawFn", "SearchStrategy", "binary", "booleans", "builds", "characters", "complex_numbers", "composite", "data", "dates", "datetimes", "decimals", "deferred", "dictionaries", "emails", "fixed_dictionaries", "floats", "fractions", "from_regex", "from_type", "frozensets", "functions", "integers", "ip_addresses", "iterables", "just", "lists", "none", "nothing", "one_of", "permutations", "random_module", "randoms", "recursive", "register_type_strategy", "runner", "sampled_from", "sets", "shared", "slices", "text", "timedeltas", "times", "timezone_keys", "timezones", "tuples", "uuids", ] def _check_exports(_public): assert set(__all__) == _public, (set(__all__) - _public, _public - set(__all__)) # Verify that all exported strategy functions were registered with # @declares_strategy. existing_strategies = set(_all_strategies) - {"_maybe_nil_uuids"} exported_strategies = set(__all__) - { "DataObject", "DrawFn", "SearchStrategy", "composite", "register_type_strategy", } assert existing_strategies == exported_strategies, ( existing_strategies - exported_strategies, exported_strategies - existing_strategies, ) _check_exports({n for n in dir() if n[0] not in "_@"}) del _check_exports ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Package defining SearchStrategy, which is the core type that Hypothesis uses to explore data.""" from .strategies import SearchStrategy, check_strategy __all__ = ["SearchStrategy", "check_strategy"] ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/attrs.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. # Since Hypothesis doesn't have a hard dependency on attrs, be careful to only import # this file when attrs is in sys.modules. from collections.abc import Collection, Generator, Iterable, Sequence from functools import reduce from itertools import chain from types import EllipsisType from typing import Any, TypeVar import attr # attr/validators.pyi does not expose types for these, even though they exist # in source. from attr.validators import ( # type: ignore _AndValidator, _InstanceOfValidator, _InValidator, _OptionalValidator, ) from attrs import Attribute, AttrsInstance, Factory from hypothesis import strategies as st from hypothesis.errors import ResolutionFailed from hypothesis.internal.compat import get_type_hints from hypothesis.strategies._internal.core import BuildsStrategy from hypothesis.strategies._internal.strategies import SearchStrategy from hypothesis.strategies._internal.types import is_a_type, type_sorting_key from hypothesis.utils.conventions import infer T = TypeVar("T") def get_attribute_by_alias( fields: Iterable[Attribute], alias: str, *, target: type[AttrsInstance] | None = None, ) -> Attribute: """ Get an attrs attribute by its alias, rather than its name (compare getattr(fields, name)). ``target`` is used only to provide a nicer error message, and can be safely omitted. """ # attrs supports defining an alias for a field, which is the name used when # defining __init__. The init args are what we pull from when determining # what parameters we need to supply to the class, so it's what we need to # match against as well, rather than the class-level attribute name. matched_fields = [f for f in fields if f.alias == alias] if not matched_fields: raise TypeError( f"Unexpected keyword argument {alias} for attrs class" f"{f' {target}' if target else ''}. Expected one of " f"{[f.name for f in fields]}" ) # alias is used as an arg in __init__, so it is guaranteed to be unique, if # it exists. assert len(matched_fields) == 1 return matched_fields[0] def from_attrs( target: type[AttrsInstance], args: tuple[SearchStrategy[Any], ...], kwargs: dict[str, SearchStrategy[Any] | EllipsisType], to_infer: Iterable[str], ) -> SearchStrategy: """An internal version of builds(), specialised for Attrs classes.""" attributes: tuple[Attribute, ...] = attr.fields(target) kwargs = {k: v for k, v in kwargs.items() if v is not infer} for name in to_infer: attrib = get_attribute_by_alias(attributes, name, target=target) kwargs[name] = from_attrs_attribute(attrib, target) # We might make this strategy more efficient if we added a layer here that # retries drawing if validation fails, for improved composition. # The treatment of timezones in datetimes() provides a precedent. return BuildsStrategy(target, args, kwargs) def from_attrs_attribute( attrib: Attribute, target: type[AttrsInstance] ) -> SearchStrategy: """Infer a strategy from the metadata on an attr.Attribute object.""" # Try inferring from the default argument. Note that this will only help if # the user passed `...` to builds() for this attribute, but in that case # we use it as the minimal example. default: SearchStrategy = st.nothing() # attr/__init__.pyi uses overloads to declare Factory as a function, not a # class. This is a fib - at runtime and always, it is a class. if isinstance(attrib.default, Factory): # type: ignore assert attrib.default is not None if not attrib.default.takes_self: default = st.builds(attrib.default.factory) elif attrib.default is not attr.NOTHING: default = st.just(attrib.default) # Try inferring None, exact values, or type from attrs provided validators. # updated to none() on seeing an OptionalValidator null: SearchStrategy = st.nothing() # list of in_ validator collections to sample from in_collections = [] # type constraints to pass to types_to_strategy() validator_types = set() if attrib.validator is not None: validator = attrib.validator if isinstance(validator, _OptionalValidator): null = st.none() validator = validator.validator if isinstance(validator, _AndValidator): vs = validator._validators else: vs = [validator] for v in vs: if isinstance(v, _InValidator): if isinstance(v.options, str): in_collections.append(list(all_substrings(v.options))) else: in_collections.append(v.options) elif isinstance(v, _InstanceOfValidator): validator_types.add(v.type) # This is the important line. We compose the final strategy from various # parts. The default value, if any, is the minimal shrink, followed by # None (again, if allowed). We then prefer to sample from values passed # to an in_ validator if available, but infer from a type otherwise. # Pick one because (sampled_from((1, 2)) | from_type(int)) would usually # fail validation by generating e.g. zero! if in_collections: sample = st.sampled_from(list(ordered_intersection(in_collections))) strat = default | null | sample else: strat = default | null | types_to_strategy(attrib, validator_types) # Better to give a meaningful error here than an opaque "could not draw" # when we try to get a value but have lost track of where this was created. if strat.is_empty: raise ResolutionFailed( "Cannot infer a strategy from the default, validator, type, or " f"converter for attribute={attrib!r} of class={target!r}" ) return strat def types_to_strategy(attrib: Attribute, types: Collection[Any]) -> SearchStrategy: """Find all the type metadata for this attribute, reconcile it, and infer a strategy from the mess.""" # If we know types from the validator(s), that's sufficient. if len(types) == 1: (typ,) = types if isinstance(typ, tuple): return st.one_of(*map(st.from_type, typ)) return st.from_type(typ) elif types: # We have a list of tuples of types, and want to find a type # (or tuple of types) that is a subclass of all of them. type_tuples = [k if isinstance(k, tuple) else (k,) for k in types] # Flatten the list, filter types that would fail validation, and # sort so that ordering is stable between runs and shrinks well. allowed = [ t for t in set(sum(type_tuples, ())) if all(issubclass(t, tup) for tup in type_tuples) ] allowed.sort(key=type_sorting_key) return st.one_of([st.from_type(t) for t in allowed]) # Otherwise, try the `type` attribute as a fallback, and finally try # the type hints on a converter (desperate!) before giving up. if is_a_type(getattr(attrib, "type", None)): assert attrib.type is not None # The convoluted test is because variable annotations may be stored # in string form; attrs doesn't evaluate them and we don't handle them. # See PEP 526, PEP 563, and Hypothesis issue #1004 for details. return st.from_type(attrib.type) converter = getattr(attrib, "converter", None) if isinstance(converter, type): return st.from_type(converter) elif callable(converter): hints = get_type_hints(converter) if "return" in hints: return st.from_type(hints["return"]) return st.nothing() def ordered_intersection(in_: Sequence[Iterable[T]]) -> Generator[T, None, None]: """Set union of n sequences, ordered for reproducibility across runs.""" intersection = reduce(set.intersection, in_, set(in_[0])) for x in chain.from_iterable(in_): if x in intersection: yield x intersection.remove(x) def all_substrings(s: str) -> Generator[str, None, None]: """Generate all substrings of `s`, in order of length then occurrence. Includes the empty string (first), and any duplicates that are present. >>> list(all_substrings('010')) ['', '0', '1', '0', '01', '10', '010'] """ yield s[:0] for n, _ in enumerate(s): for i in range(len(s) - n): yield s[i : i + n + 1] ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/collections.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import copy import math from collections.abc import Callable, Iterable from typing import Any, overload from hypothesis import strategies as st from hypothesis.control import current_build_context from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.engine import BUFFER_SIZE from hypothesis.internal.conjecture.junkdrawer import LazySequenceCopy from hypothesis.internal.conjecture.utils import combine_labels from hypothesis.internal.filtering import get_integer_predicate_bounds from hypothesis.internal.reflection import is_identity_function from hypothesis.strategies._internal.strategies import ( T3, T4, T5, Ex, FilteredStrategy, RecurT, SampledFromStrategy, SearchStrategy, T, check_strategy, filter_not_satisfied, ) from hypothesis.strategies._internal.utils import cacheable, defines_strategy from hypothesis.utils.conventions import UniqueIdentifier from hypothesis.vendor.pretty import ( ArgLabelsT, IDKey, _fixeddict_pprinter, _tuple_pprinter, ) class TupleStrategy(SearchStrategy[tuple[Ex, ...]]): """A strategy responsible for fixed length tuples based on heterogeneous strategies for each of their elements.""" def __init__(self, strategies: Iterable[SearchStrategy[Any]]): super().__init__() self.element_strategies = tuple(strategies) def do_validate(self) -> None: for s in self.element_strategies: s.validate() def calc_label(self) -> int: return combine_labels( self.class_label, *(s.label for s in self.element_strategies) ) def __repr__(self) -> str: tuple_string = ", ".join(map(repr, self.element_strategies)) return f"TupleStrategy(({tuple_string}))" def calc_has_reusable_values(self, recur: RecurT) -> bool: return all(recur(e) for e in self.element_strategies) def do_draw(self, data: ConjectureData) -> tuple[Ex, ...]: context = current_build_context() arg_labels: ArgLabelsT = {} result = [] for i, strategy in enumerate(self.element_strategies): with context.track_arg_label(f"arg[{i}]") as arg_label: result.append(data.draw(strategy)) arg_labels |= arg_label result = tuple(result) if arg_labels: context.known_object_printers[IDKey(result)].append( _tuple_pprinter(arg_labels) ) return result def calc_is_empty(self, recur: RecurT) -> bool: return any(recur(e) for e in self.element_strategies) @overload def tuples() -> SearchStrategy[tuple[()]]: # pragma: no cover ... @overload def tuples(__a1: SearchStrategy[Ex]) -> SearchStrategy[tuple[Ex]]: # pragma: no cover ... @overload def tuples( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T] ) -> SearchStrategy[tuple[Ex, T]]: # pragma: no cover ... @overload def tuples( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3] ) -> SearchStrategy[tuple[Ex, T, T3]]: # pragma: no cover ... @overload def tuples( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3], __a4: SearchStrategy[T4], ) -> SearchStrategy[tuple[Ex, T, T3, T4]]: # pragma: no cover ... @overload def tuples( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3], __a4: SearchStrategy[T4], __a5: SearchStrategy[T5], ) -> SearchStrategy[tuple[Ex, T, T3, T4, T5]]: # pragma: no cover ... @overload def tuples( *args: SearchStrategy[Any], ) -> SearchStrategy[tuple[Any, ...]]: # pragma: no cover ... @cacheable @defines_strategy() def tuples(*args: SearchStrategy[Any]) -> SearchStrategy[tuple[Any, ...]]: """Return a strategy which generates a tuple of the same length as args by generating the value at index i from args[i]. e.g. tuples(integers(), integers()) would generate a tuple of length two with both values an integer. Examples from this strategy shrink by shrinking their component parts. """ for arg in args: check_strategy(arg) return TupleStrategy(args) class ListStrategy(SearchStrategy[list[Ex]]): """A strategy for lists which takes a strategy for its elements and the allowed lengths, and generates lists with the correct size and contents.""" _nonempty_filters: tuple[Callable[[Any], Any], ...] = (bool, len, tuple, list) def __init__( self, elements: SearchStrategy[Ex], min_size: int = 0, max_size: float | int | None = math.inf, ): super().__init__() self.min_size = min_size or 0 self.max_size = max_size if max_size is not None else math.inf assert 0 <= self.min_size <= self.max_size self.average_size = min( max(self.min_size * 2, self.min_size + 5), 0.5 * (self.min_size + self.max_size), ) self.element_strategy = elements if min_size > BUFFER_SIZE: raise InvalidArgument( f"{self!r} can never generate an example, because min_size is larger " "than Hypothesis supports. Including it is at best slowing down your " "tests for no benefit; at worst making them fail (maybe flakily) with " "a HealthCheck error." ) def calc_label(self) -> int: return combine_labels(self.class_label, self.element_strategy.label) def do_validate(self) -> None: self.element_strategy.validate() if self.is_empty: raise InvalidArgument( "Cannot create non-empty lists with elements drawn from " f"strategy {self.element_strategy!r} because it has no values." ) if self.element_strategy.is_empty and 0 < self.max_size < float("inf"): raise InvalidArgument( f"Cannot create a collection of max_size={self.max_size!r}, " "because no elements can be drawn from the element strategy " f"{self.element_strategy!r}" ) def calc_is_empty(self, recur: RecurT) -> bool: if self.min_size == 0: return False return recur(self.element_strategy) def do_draw(self, data: ConjectureData) -> list[Ex]: if self.element_strategy.is_empty: assert self.min_size == 0 return [] elements = cu.many( data, min_size=self.min_size, max_size=self.max_size, average_size=self.average_size, ) result = [] while elements.more(): result.append(data.draw(self.element_strategy)) return result def __repr__(self) -> str: return ( f"{self.__class__.__name__}({self.element_strategy!r}, " f"min_size={self.min_size:_}, max_size={self.max_size:_})" ) def filter(self, condition: Callable[[list[Ex]], Any]) -> SearchStrategy[list[Ex]]: if condition in self._nonempty_filters or is_identity_function(condition): assert self.max_size >= 1, "Always-empty is special cased in st.lists()" if self.min_size >= 1: return self new = copy.copy(self) new.min_size = 1 return new constraints, pred = get_integer_predicate_bounds(condition) if constraints.get("len") and ( "min_value" in constraints or "max_value" in constraints ): new = copy.copy(self) new.min_size = max( self.min_size, constraints.get("min_value", self.min_size) ) new.max_size = min( self.max_size, constraints.get("max_value", self.max_size) ) # Unsatisfiable filters are easiest to understand without rewriting. if new.min_size > new.max_size: return SearchStrategy.filter(self, condition) # Recompute average size; this is cheaper than making it into a property. new.average_size = min( max(new.min_size * 2, new.min_size + 5), 0.5 * (new.min_size + new.max_size), ) if pred is None: return new return SearchStrategy.filter(new, condition) return SearchStrategy.filter(self, condition) class UniqueListStrategy(ListStrategy[Ex]): def __init__( self, elements: SearchStrategy[Ex], min_size: int, max_size: float | int | None, # TODO: keys are guaranteed to be Hashable, not just Any, but this makes # other things harder to type keys: tuple[Callable[[Ex], Any], ...], tuple_suffixes: SearchStrategy[tuple[Ex, ...]] | None, ): super().__init__(elements, min_size, max_size) self.keys = keys self.tuple_suffixes = tuple_suffixes def do_draw(self, data: ConjectureData) -> list[Ex]: if self.element_strategy.is_empty: assert self.min_size == 0 return [] elements = cu.many( data, min_size=self.min_size, max_size=self.max_size, average_size=self.average_size, ) seen_sets: tuple[set[Ex], ...] = tuple(set() for _ in self.keys) # actually list[Ex], but if self.tuple_suffixes is present then Ex is a # tuple[T, ...] because self.element_strategy is a TuplesStrategy, and # appending a concrete tuple to `result: list[Ex]` makes mypy unhappy # without knowing that Ex = tuple. result: list[Any] = [] # We construct a filtered strategy here rather than using a check-and-reject # approach because some strategies have special logic for generation under a # filter, and FilteredStrategy can consolidate multiple filters. def not_yet_in_unique_list(val: Ex) -> bool: # type: ignore # covariant type param return all( key(val) not in seen for key, seen in zip(self.keys, seen_sets, strict=True) ) filtered = FilteredStrategy( self.element_strategy, conditions=(not_yet_in_unique_list,) ) while elements.more(): value = filtered.do_filtered_draw(data) if value is filter_not_satisfied: elements.reject(f"Aborted test because unable to satisfy {filtered!r}") else: assert not isinstance(value, UniqueIdentifier) for key, seen in zip(self.keys, seen_sets, strict=True): seen.add(key(value)) if self.tuple_suffixes is not None: value = (value, *data.draw(self.tuple_suffixes)) # type: ignore result.append(value) assert self.max_size >= len(result) >= self.min_size return result class UniqueSampledListStrategy(UniqueListStrategy): def do_draw(self, data: ConjectureData) -> list[Ex]: assert isinstance(self.element_strategy, SampledFromStrategy) should_draw = cu.many( data, min_size=self.min_size, max_size=self.max_size, average_size=self.average_size, ) seen_sets: tuple[set[Ex], ...] = tuple(set() for _ in self.keys) result: list[Any] = [] remaining = LazySequenceCopy(self.element_strategy.elements) while remaining and should_draw.more(): j = data.draw_integer(0, len(remaining) - 1) value = self.element_strategy._transform(remaining.pop(j)) if value is not filter_not_satisfied and all( key(value) not in seen for key, seen in zip(self.keys, seen_sets, strict=True) ): for key, seen in zip(self.keys, seen_sets, strict=True): seen.add(key(value)) if self.tuple_suffixes is not None: value = (value, *data.draw(self.tuple_suffixes)) result.append(value) else: should_draw.reject( "UniqueSampledListStrategy filter not satisfied or value already seen" ) assert self.max_size >= len(result) >= self.min_size return result class FixedDictStrategy(SearchStrategy[dict[Any, Any]]): """A strategy which produces dicts with a fixed set of keys, given a strategy for each of their equivalent values. e.g. {'foo' : some_int_strategy} would generate dicts with the single key 'foo' mapping to some integer. """ def __init__( self, mapping: dict[Any, SearchStrategy[Any]], *, optional: dict[Any, SearchStrategy[Any]] | None, ): super().__init__() dict_type = type(mapping) self.mapping = mapping keys = tuple(mapping.keys()) self.fixed = st.tuples(*[mapping[k] for k in keys]).map( lambda value: dict_type(zip(keys, value, strict=True)) ) self.optional = optional def do_draw(self, data: ConjectureData) -> dict[Any, Any]: context = current_build_context() arg_labels: ArgLabelsT = {} value = type(self.mapping)() for key, strategy in self.mapping.items(): with context.track_arg_label(str(key)) as arg_label: value[key] = data.draw(strategy) arg_labels |= arg_label if self.optional is not None: remaining = [k for k, v in self.optional.items() if not v.is_empty] should_draw = cu.many( data, min_size=0, max_size=len(remaining), average_size=len(remaining) / 2, ) while should_draw.more(): j = data.draw_integer(0, len(remaining) - 1) remaining[-1], remaining[j] = remaining[j], remaining[-1] key = remaining.pop() with context.track_arg_label(str(key)) as arg_label: value[key] = data.draw(self.optional[key]) arg_labels |= arg_label if arg_labels: context.known_object_printers[IDKey(value)].append( _fixeddict_pprinter(arg_labels, self.mapping) ) return value def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.fixed) def __repr__(self) -> str: if self.optional is not None: return f"fixed_dictionaries({self.mapping!r}, optional={self.optional!r})" return f"fixed_dictionaries({self.mapping!r})" ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/core.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import codecs import enum import math import operator import random import re import string import sys import typing import warnings from collections.abc import Callable, Collection, Hashable, Iterable, Sequence from contextvars import ContextVar from decimal import Context, Decimal, localcontext from fractions import Fraction from functools import reduce from inspect import Parameter, Signature, isabstract, isclass from re import Pattern from types import EllipsisType, FunctionType, GenericAlias from typing import ( Annotated, Any, AnyStr, Concatenate, Literal, NewType, NoReturn, ParamSpec, Protocol, TypeAlias, TypeVar, cast, get_args, get_origin, overload, ) from uuid import UUID from hypothesis.control import ( cleanup, current_build_context, deprecate_random_in_strategy, note, should_note, ) from hypothesis.errors import ( HypothesisSideeffectWarning, HypothesisWarning, InvalidArgument, ResolutionFailed, RewindRecursive, SmallSearchSpaceWarning, ) from hypothesis.internal.cathetus import cathetus from hypothesis.internal.charmap import ( Categories, CategoryName, as_general_categories, categories as all_categories, ) from hypothesis.internal.compat import ( bit_count, ceil, floor, get_type_hints, is_typed_named_tuple, ) from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.utils import ( calc_label_from_callable, calc_label_from_name, check_sample, combine_labels, identity, ) from hypothesis.internal.entropy import get_seeder_and_restorer from hypothesis.internal.floats import float_of from hypothesis.internal.reflection import ( define_function_signature, get_pretty_function_description, get_signature, is_first_param_referenced_in_function, nicerepr, repr_call, required_args, ) from hypothesis.internal.validation import ( check_type, check_valid_integer, check_valid_interval, check_valid_magnitude, check_valid_size, check_valid_sizes, try_convert, ) from hypothesis.strategies._internal import SearchStrategy, check_strategy from hypothesis.strategies._internal.collections import ( FixedDictStrategy, ListStrategy, TupleStrategy, UniqueListStrategy, UniqueSampledListStrategy, tuples, ) from hypothesis.strategies._internal.deferred import DeferredStrategy from hypothesis.strategies._internal.functions import FunctionStrategy from hypothesis.strategies._internal.lazy import LazyStrategy, unwrap_strategies from hypothesis.strategies._internal.misc import BooleansStrategy, just, none, nothing from hypothesis.strategies._internal.numbers import ( IntegersStrategy, Real, floats, integers, ) from hypothesis.strategies._internal.recursive import RecursiveStrategy from hypothesis.strategies._internal.shared import SharedStrategy from hypothesis.strategies._internal.strategies import ( Ex, SampledFromStrategy, T, one_of, ) from hypothesis.strategies._internal.strings import ( BytesStrategy, OneCharStringStrategy, TextStrategy, _check_is_single_character, ) from hypothesis.strategies._internal.utils import cacheable, defines_strategy from hypothesis.utils.conventions import not_set from hypothesis.utils.deprecation import note_deprecation from hypothesis.vendor.pretty import ArgLabelsT, RepresentationPrinter @cacheable @defines_strategy(force_reusable_values=True) def booleans() -> SearchStrategy[bool]: """Returns a strategy which generates instances of :class:`python:bool`. Examples from this strategy will shrink towards ``False`` (i.e. shrinking will replace ``True`` with ``False`` where possible). """ return BooleansStrategy() @overload def sampled_from(elements: Sequence[T]) -> SearchStrategy[T]: # pragma: no cover ... @overload def sampled_from(elements: type[enum.Enum]) -> SearchStrategy[Any]: # pragma: no cover # `SearchStrategy[Enum]` is unreliable due to metaclass issues. ... @overload def sampled_from( elements: type[enum.Enum] | Sequence[Any], ) -> SearchStrategy[Any]: # pragma: no cover ... @defines_strategy(eager="try") def sampled_from( elements: type[enum.Enum] | Sequence[Any], ) -> SearchStrategy[Any]: """Returns a strategy which generates any value present in ``elements``. Note that as with :func:`~hypothesis.strategies.just`, values will not be copied and thus you should be careful of using mutable data. ``sampled_from`` supports ordered collections, as well as :class:`~python:enum.Enum` objects. :class:`~python:enum.Flag` objects may also generate any combination of their members. Examples from this strategy shrink by replacing them with values earlier in the list. So e.g. ``sampled_from([10, 1])`` will shrink by trying to replace 1 values with 10, and ``sampled_from([1, 10])`` will shrink by trying to replace 10 values with 1. It is an error to sample from an empty sequence, because returning :func:`nothing` makes it too easy to silently drop parts of compound strategies. If you need that behaviour, use ``sampled_from(seq) if seq else nothing()``. """ values = check_sample(elements, "sampled_from") force_repr = None # check_sample converts to tuple unconditionally, but we want to preserve # square braces for list reprs. # This will not cover custom sequence implementations which return different # braces (or other, more unusual things) for their reprs, but this is a tradeoff # between repr accuracy and greedily-evaluating all sequence reprs (at great # cost for large sequences). force_repr_braces = ("[", "]") if isinstance(elements, list) else None if isinstance(elements, type) and issubclass(elements, enum.Enum): force_repr = f"sampled_from({elements.__module__}.{elements.__name__})" if isclass(elements) and issubclass(elements, enum.Flag): # Combinations of enum.Flag members (including empty) are also members. We generate these # dynamically, because static allocation takes O(2^n) memory. LazyStrategy is used for the # ease of force_repr. # Add all named values, both flag bits (== list(elements)) and aliases. The aliases are # necessary for full coverage for flags that would fail enum.NAMED_FLAGS check, and they # are also nice values to shrink to. flags = sorted( set(elements.__members__.values()), key=lambda v: (bit_count(v.value), v.value), ) # Finally, try to construct the empty state if it is not named. It's placed at the # end so that we shrink to named values. flags_with_empty = flags if not flags or flags[0].value != 0: try: flags_with_empty = [*flags, elements(0)] except TypeError: # pragma: no cover # Happens on some python versions (at least 3.12) when there are no named values pass inner = [ # Consider one or no named flags set, with shrink-to-named-flag behaviour. # Special cases (length zero or one) are handled by the inner sampled_from. sampled_from(flags_with_empty), ] if len(flags) > 1: inner += [ # Uniform distribution over number of named flags or combinations set. The overlap # at r=1 is intentional, it may lead to oversampling but gives consistent shrinking # behaviour. integers(min_value=1, max_value=len(flags)) .flatmap(lambda r: sets(sampled_from(flags), min_size=r, max_size=r)) .map(lambda s: elements(reduce(operator.or_, s))), ] return LazyStrategy(one_of, args=inner, kwargs={}, force_repr=force_repr) if not values: def has_annotations(elements): if sys.version_info[:2] < (3, 14): return vars(elements).get("__annotations__") else: # pragma: no cover # covered by 3.14 tests import annotationlib return bool(annotationlib.get_annotations(elements)) if ( isinstance(elements, type) and issubclass(elements, enum.Enum) and has_annotations(elements) ): # See https://github.com/HypothesisWorks/hypothesis/issues/2923 raise InvalidArgument( f"Cannot sample from {elements.__module__}.{elements.__name__} " "because it contains no elements. It does however have annotations, " "so maybe you tried to write an enum as if it was a dataclass?" ) raise InvalidArgument("Cannot sample from a length-zero sequence.") if len(values) == 1: return just(values[0]) return SampledFromStrategy( values, force_repr=force_repr, force_repr_braces=force_repr_braces ) def _gets_first_item(fn: Callable) -> bool: # Introspection for either `itemgetter(0)`, or `lambda x: x[0]` if isinstance(fn, FunctionType): s = get_pretty_function_description(fn) return bool(re.fullmatch(s, r"lambda ([a-z]+): \1\[0\]")) return isinstance(fn, operator.itemgetter) and repr(fn) == "operator.itemgetter(0)" @cacheable @defines_strategy() def lists( elements: SearchStrategy[Ex], *, min_size: int = 0, max_size: int | None = None, unique_by: ( None | Callable[[Ex], Hashable] | tuple[Callable[[Ex], Hashable], ...] ) = None, unique: bool = False, ) -> SearchStrategy[list[Ex]]: """Returns a list containing values drawn from elements with length in the interval [min_size, max_size] (no bounds in that direction if these are None). If max_size is 0, only the empty list will be drawn. If ``unique`` is True (or something that evaluates to True), we compare direct object equality, as if unique_by was ``lambda x: x``. This comparison only works for hashable types. If ``unique_by`` is not None it must be a callable or tuple of callables returning a hashable type when given a value drawn from elements. The resulting list will satisfy the condition that for ``i`` != ``j``, ``unique_by(result[i])`` != ``unique_by(result[j])``. If ``unique_by`` is a tuple of callables the uniqueness will be respective to each callable. For example, the following will produce two columns of integers with both columns being unique respectively. .. code-block:: pycon >>> twoints = st.tuples(st.integers(), st.integers()) >>> st.lists(twoints, unique_by=(lambda x: x[0], lambda x: x[1])) Examples from this strategy shrink by trying to remove elements from the list, and by shrinking each individual element of the list. """ check_valid_sizes(min_size, max_size) check_strategy(elements, "elements") if unique: if unique_by is not None: raise InvalidArgument( "cannot specify both unique and unique_by " "(you probably only want to set unique_by)" ) else: unique_by = identity if max_size == 0: return builds(list) if unique_by is not None: if not (callable(unique_by) or isinstance(unique_by, tuple)): raise InvalidArgument( f"{unique_by=} is not a callable or tuple of callables" ) if callable(unique_by): unique_by = (unique_by,) if len(unique_by) == 0: raise InvalidArgument("unique_by is empty") for i, f in enumerate(unique_by): if not callable(f): raise InvalidArgument(f"unique_by[{i}]={f!r} is not a callable") # Note that lazy strategies automatically unwrap when passed to a defines_strategy # function. tuple_suffixes = None if ( # We're generating a list of tuples unique by the first element, perhaps # via st.dictionaries(), and this will be more efficient if we rearrange # our strategy somewhat to draw the first element then draw add the rest. isinstance(elements, TupleStrategy) and len(elements.element_strategies) >= 1 and all(_gets_first_item(fn) for fn in unique_by) ): unique_by = (identity,) tuple_suffixes = TupleStrategy(elements.element_strategies[1:]) elements = elements.element_strategies[0] # UniqueSampledListStrategy offers a substantial performance improvement for # unique arrays with few possible elements, e.g. of eight-bit integer types. if ( isinstance(elements, IntegersStrategy) and elements.start is not None and elements.end is not None and (elements.end - elements.start) <= 255 ): elements = SampledFromStrategy( sorted(range(elements.start, elements.end + 1), key=abs) # type: ignore if elements.end < 0 or elements.start > 0 else ( list(range(elements.end + 1)) + list(range(-1, elements.start - 1, -1)) ) ) if isinstance(elements, SampledFromStrategy): element_count = len(elements.elements) if min_size > element_count: raise InvalidArgument( f"Cannot create a collection of {min_size=} unique " f"elements with values drawn from only {element_count} distinct " "elements" ) if max_size is not None: max_size = min(max_size, element_count) else: max_size = element_count return UniqueSampledListStrategy( elements=elements, max_size=max_size, min_size=min_size, keys=unique_by, tuple_suffixes=tuple_suffixes, ) return UniqueListStrategy( elements=elements, max_size=max_size, min_size=min_size, keys=unique_by, tuple_suffixes=tuple_suffixes, ) return ListStrategy(elements, min_size=min_size, max_size=max_size) @cacheable @defines_strategy() def sets( elements: SearchStrategy[Ex], *, min_size: int = 0, max_size: int | None = None, ) -> SearchStrategy[set[Ex]]: """This has the same behaviour as lists, but returns sets instead. Note that Hypothesis cannot tell if values are drawn from elements are hashable until running the test, so you can define a strategy for sets of an unhashable type but it will fail at test time. Examples from this strategy shrink by trying to remove elements from the set, and by shrinking each individual element of the set. """ return lists( elements=elements, min_size=min_size, max_size=max_size, unique=True ).map(set) @cacheable @defines_strategy() def frozensets( elements: SearchStrategy[Ex], *, min_size: int = 0, max_size: int | None = None, ) -> SearchStrategy[frozenset[Ex]]: """This is identical to the sets function but instead returns frozensets.""" return lists( elements=elements, min_size=min_size, max_size=max_size, unique=True ).map(frozenset) class PrettyIter: def __init__(self, values): self._values = values self._iter = iter(self._values) def __iter__(self): return self._iter def __next__(self): return next(self._iter) def __repr__(self) -> str: return f"iter({self._values!r})" @defines_strategy() def iterables( elements: SearchStrategy[Ex], *, min_size: int = 0, max_size: int | None = None, unique_by: ( None | Callable[[Ex], Hashable] | tuple[Callable[[Ex], Hashable], ...] ) = None, unique: bool = False, ) -> SearchStrategy[Iterable[Ex]]: """This has the same behaviour as lists, but returns iterables instead. Some iterables cannot be indexed (e.g. sets) and some do not have a fixed length (e.g. generators). This strategy produces iterators, which cannot be indexed and do not have a fixed length. This ensures that you do not accidentally depend on sequence behaviour. """ return lists( elements=elements, min_size=min_size, max_size=max_size, unique_by=unique_by, unique=unique, ).map(PrettyIter) # this type definition is imprecise, in multiple ways: # * mapping and optional can be of different types: # s: dict[str | int, int] = st.fixed_dictionaries( # {"a": st.integers()}, optional={1: st.integers()} # ) # * the values in either mapping or optional need not all be of the same type: # s: dict[str, int | bool] = st.fixed_dictionaries( # {"a": st.integers(), "b": st.booleans()} # ) # * the arguments may be of any dict-compatible type, in which case the return # value will be of that type instead of dit # # Overloads may help here, but I doubt we'll be able to satisfy all these # constraints. # # Here's some platonic ideal test cases for revealed_types.py, with the understanding # that some may not be achievable: # # ("fixed_dictionaries({'a': booleans()})", "dict[str, bool]"), # ("fixed_dictionaries({'a': booleans(), 'b': integers()})", "dict[str, bool | int]"), # ("fixed_dictionaries({}, optional={'a': booleans()})", "dict[str, bool]"), # ( # "fixed_dictionaries({'a': booleans()}, optional={1: booleans()})", # "dict[str | int, bool]", # ), # ( # "fixed_dictionaries({'a': booleans()}, optional={1: integers()})", # "dict[str | int, bool | int]", # ), @defines_strategy() def fixed_dictionaries( mapping: dict[T, SearchStrategy[Ex]], *, optional: dict[T, SearchStrategy[Ex]] | None = None, ) -> SearchStrategy[dict[T, Ex]]: """Generates a dictionary of the same type as mapping with a fixed set of keys mapping to strategies. ``mapping`` must be a dict subclass. Generated values have all keys present in mapping, in iteration order, with the corresponding values drawn from mapping[key]. If ``optional`` is passed, the generated value *may or may not* contain each key from ``optional`` and a value drawn from the corresponding strategy. Generated values may contain optional keys in an arbitrary order. Examples from this strategy shrink by shrinking each individual value in the generated dictionary, and omitting optional key-value pairs. """ check_type(dict, mapping, "mapping") for k, v in mapping.items(): check_strategy(v, f"mapping[{k!r}]") if optional is not None: check_type(dict, optional, "optional") for k, v in optional.items(): check_strategy(v, f"optional[{k!r}]") if type(mapping) != type(optional): raise InvalidArgument( f"Got arguments of different types: " f"mapping={nicerepr(type(mapping))}, " f"optional={nicerepr(type(optional))}" ) if set(mapping) & set(optional): raise InvalidArgument( "The following keys were in both mapping and optional, " f"which is invalid: {set(mapping) & set(optional)!r}" ) return FixedDictStrategy(mapping, optional=optional) _get_first_item = operator.itemgetter(0) @cacheable @defines_strategy() def dictionaries( keys: SearchStrategy[Ex], values: SearchStrategy[T], *, dict_class: type = dict, min_size: int = 0, max_size: int | None = None, ) -> SearchStrategy[dict[Ex, T]]: # Describing the exact dict_class to Mypy drops the key and value types, # so we report Dict[K, V] instead of Mapping[Any, Any] for now. Sorry! """Generates dictionaries of type ``dict_class`` with keys drawn from the ``keys`` argument and values drawn from the ``values`` argument. The size parameters have the same interpretation as for :func:`~hypothesis.strategies.lists`. Examples from this strategy shrink by trying to remove keys from the generated dictionary, and by shrinking each generated key and value. """ check_valid_sizes(min_size, max_size) if max_size == 0: return fixed_dictionaries(dict_class()) check_strategy(keys, "keys") check_strategy(values, "values") return lists( tuples(keys, values), min_size=min_size, max_size=max_size, unique_by=_get_first_item, ).map(dict_class) @cacheable @defines_strategy(force_reusable_values=True) def characters( *, codec: str | None = None, min_codepoint: int | None = None, max_codepoint: int | None = None, categories: Collection[CategoryName] | None = None, exclude_categories: Collection[CategoryName] | None = None, exclude_characters: Collection[str] | None = None, include_characters: Collection[str] | None = None, # Note: these arguments are deprecated aliases for backwards compatibility blacklist_categories: Collection[CategoryName] | None = None, whitelist_categories: Collection[CategoryName] | None = None, blacklist_characters: Collection[str] | None = None, whitelist_characters: Collection[str] | None = None, ) -> SearchStrategy[str]: r"""Generates characters, length-one :class:`python:str`\ ings, following specified filtering rules. - When no filtering rules are specified, any character can be produced. - If ``min_codepoint`` or ``max_codepoint`` is specified, then only characters having a codepoint in that range will be produced. - If ``categories`` is specified, then only characters from those Unicode categories will be produced. This is a further restriction, characters must also satisfy ``min_codepoint`` and ``max_codepoint``. - If ``exclude_categories`` is specified, then any character from those categories will not be produced. You must not pass both ``categories`` and ``exclude_categories``; these arguments are alternative ways to specify exactly the same thing. - If ``include_characters`` is specified, then any additional characters in that list will also be produced. - If ``exclude_characters`` is specified, then any characters in that list will be not be produced. Any overlap between ``include_characters`` and ``exclude_characters`` will raise an exception. - If ``codec`` is specified, only characters in the specified `codec encodings`_ will be produced. The ``_codepoint`` arguments must be integers between zero and :obj:`python:sys.maxunicode`. The ``_characters`` arguments must be collections of length-one unicode strings, such as a unicode string. The ``_categories`` arguments must be used to specify either the one-letter Unicode major category or the two-letter Unicode `general category`_. For example, ``('Nd', 'Lu')`` signifies "Number, decimal digit" and "Letter, uppercase". A single letter ('major category') can be given to match all corresponding categories, for example ``'P'`` for characters in any punctuation category. We allow codecs from the :mod:`codecs` module and their aliases, platform specific and user-registered codecs if they are available, and `python-specific text encodings`_ (but not text or binary transforms). ``include_characters`` which cannot be encoded using this codec will raise an exception. If non-encodable codepoints or categories are explicitly allowed, the ``codec`` argument will exclude them without raising an exception. .. _general category: https://en.wikipedia.org/wiki/Unicode_character_property .. _codec encodings: https://docs.python.org/3/library/codecs.html#encodings-and-unicode .. _python-specific text encodings: https://docs.python.org/3/library/codecs.html#python-specific-encodings Examples from this strategy shrink towards the codepoint for ``'0'``, or the first allowable codepoint after it if ``'0'`` is excluded. """ check_valid_size(min_codepoint, "min_codepoint") check_valid_size(max_codepoint, "max_codepoint") check_valid_interval(min_codepoint, max_codepoint, "min_codepoint", "max_codepoint") categories = cast(Categories | None, categories) if categories is not None and exclude_categories is not None: raise InvalidArgument( f"Pass at most one of {categories=} and {exclude_categories=} - " "these arguments both specify which categories are allowed, so it " "doesn't make sense to use both in a single call." ) # Handle deprecation of whitelist/blacklist arguments has_old_arg = any(v is not None for k, v in locals().items() if "list" in k) has_new_arg = any(v is not None for k, v in locals().items() if "lude" in k) if has_old_arg and has_new_arg: raise InvalidArgument( "The deprecated blacklist/whitelist arguments cannot be used in " "the same call as their replacement include/exclude arguments." ) if blacklist_categories is not None: exclude_categories = blacklist_categories if whitelist_categories is not None: categories = whitelist_categories if blacklist_characters is not None: exclude_characters = blacklist_characters if whitelist_characters is not None: include_characters = whitelist_characters if ( min_codepoint is None and max_codepoint is None and categories is None and exclude_categories is None and include_characters is not None and codec is None ): raise InvalidArgument( "Nothing is excluded by other arguments, so passing only " f"{include_characters=} would have no effect. " "Also pass categories=(), or use " f"sampled_from({include_characters!r}) instead." ) exclude_characters = exclude_characters or "" include_characters = include_characters or "" if not_one_char := [c for c in exclude_characters if len(c) != 1]: raise InvalidArgument( "Elements of exclude_characters are required to be a single character, " f"but {not_one_char!r} passed in {exclude_characters=} was not." ) if not_one_char := [c for c in include_characters if len(c) != 1]: raise InvalidArgument( "Elements of include_characters are required to be a single character, " f"but {not_one_char!r} passed in {include_characters=} was not." ) overlap = set(exclude_characters).intersection(include_characters) if overlap: raise InvalidArgument( f"Characters {sorted(overlap)!r} are present in both " f"{include_characters=} and {exclude_characters=}" ) if categories is not None: categories = as_general_categories(categories, "categories") if exclude_categories is not None: exclude_categories = as_general_categories( exclude_categories, "exclude_categories" ) if categories is not None and not categories and not include_characters: raise InvalidArgument( "When `categories` is an empty collection and there are " "no characters specified in include_characters, nothing can " "be generated by the characters() strategy." ) both_cats = set(exclude_categories or ()).intersection(categories or ()) if both_cats: # Note: we check that exactly one of `categories` or `exclude_categories` is # passed above, but retain this older check for the deprecated arguments. raise InvalidArgument( f"Categories {sorted(both_cats)!r} are present in both " f"{categories=} and {exclude_categories=}" ) elif exclude_categories is not None: categories = set(all_categories()) - set(exclude_categories) del exclude_categories if codec is not None: try: codec = codecs.lookup(codec).name # Check this is not a str-to-str or bytes-to-bytes codec; see # https://docs.python.org/3/library/codecs.html#binary-transforms "".encode(codec) except LookupError: raise InvalidArgument(f"{codec=} is not valid on this system") from None except Exception: raise InvalidArgument(f"{codec=} is not a valid codec") from None for char in include_characters: try: char.encode(encoding=codec, errors="strict") except UnicodeEncodeError: raise InvalidArgument( f"Character {char!r} in {include_characters=} " f"cannot be encoded with {codec=}" ) from None # ascii and utf-8 are sufficient common that we have faster special handling if codec == "ascii": if (max_codepoint is None) or (max_codepoint > 127): max_codepoint = 127 codec = None elif codec == "utf-8": if categories is None: categories = all_categories() categories = tuple(c for c in categories if c != "Cs") return OneCharStringStrategy.from_characters_args( categories=categories, exclude_characters=exclude_characters, min_codepoint=min_codepoint, max_codepoint=max_codepoint, include_characters=include_characters, codec=codec, ) # Hide the deprecated aliases from documentation and casual inspection characters.__signature__ = (__sig := get_signature(characters)).replace( # type: ignore parameters=[p for p in __sig.parameters.values() if "list" not in p.name] ) @cacheable @defines_strategy(force_reusable_values=True) def text( alphabet: Collection[str] | SearchStrategy[str] = characters(codec="utf-8"), *, min_size: int = 0, max_size: int | None = None, ) -> SearchStrategy[str]: """Generates strings with characters drawn from ``alphabet``, which should be a collection of length one strings or a strategy generating such strings. The default alphabet strategy can generate the full unicode range but excludes surrogate characters because they are invalid in the UTF-8 encoding. You can use :func:`~hypothesis.strategies.characters` without arguments to find surrogate-related bugs such as :bpo:`34454`. ``min_size`` and ``max_size`` have the usual interpretations. Note that Python measures string length by counting codepoints: U+00C5 ``Å`` is a single character, while U+0041 U+030A ``Å`` is two - the ``A``, and a combining ring above. Examples from this strategy shrink towards shorter strings, and with the characters in the text shrinking as per the alphabet strategy. This strategy does not :func:`~python:unicodedata.normalize` examples, so generated strings may be in any or none of the 'normal forms'. """ check_valid_sizes(min_size, max_size) if isinstance(alphabet, SearchStrategy): char_strategy = unwrap_strategies(alphabet) if isinstance(char_strategy, SampledFromStrategy): # Check this via the up-front validation logic below, and incidentally # convert into a `characters()` strategy for standard text shrinking. return text(char_strategy.elements, min_size=min_size, max_size=max_size) elif not isinstance(char_strategy, OneCharStringStrategy): char_strategy = char_strategy.map(_check_is_single_character) else: non_string = [c for c in alphabet if not isinstance(c, str)] if non_string: raise InvalidArgument( "The following elements in alphabet are not unicode " f"strings: {non_string!r}" ) not_one_char = [c for c in alphabet if len(c) != 1] if not_one_char: raise InvalidArgument( "The following elements in alphabet are not of length one, " f"which leads to violation of size constraints: {not_one_char!r}" ) if alphabet in ["ascii", "utf-8"]: warnings.warn( f"st.text({alphabet!r}): it seems like you are trying to use the " f"codec {alphabet!r}. st.text({alphabet!r}) instead generates " f"strings using the literal characters {list(alphabet)!r}. To specify " f"the {alphabet} codec, use st.text(st.characters(codec={alphabet!r})). " "If you intended to use character literals, you can silence this " "warning by reordering the characters.", HypothesisWarning, # this stacklevel is of course incorrect, but breaking out of the # levels of LazyStrategy and validation isn't worthwhile. stacklevel=1, ) char_strategy = ( characters(categories=(), include_characters=alphabet) if alphabet else nothing() ) if (max_size == 0 or char_strategy.is_empty) and not min_size: return just("") # mypy is unhappy with ListStrategy(SearchStrategy[list[Ex]]) and then TextStrategy # setting Ex = str. Mypy is correct to complain because we have an LSP violation # here in the TextStrategy.do_draw override. Would need refactoring to resolve. return TextStrategy(char_strategy, min_size=min_size, max_size=max_size) # type: ignore @overload def from_regex( regex: bytes | Pattern[bytes], *, fullmatch: bool = False, ) -> SearchStrategy[bytes]: # pragma: no cover ... @overload def from_regex( regex: str | Pattern[str], *, fullmatch: bool = False, alphabet: str | SearchStrategy[str] = characters(codec="utf-8"), ) -> SearchStrategy[str]: # pragma: no cover ... @cacheable @defines_strategy() def from_regex( regex: AnyStr | Pattern[AnyStr], *, fullmatch: bool = False, alphabet: str | SearchStrategy[str] | None = None, ) -> SearchStrategy[AnyStr]: r"""Generates strings that contain a match for the given regex (i.e. ones for which :func:`python:re.search` will return a non-None result). ``regex`` may be a pattern or :func:`compiled regex `. Both byte-strings and unicode strings are supported, and will generate examples of the same type. You can use regex flags such as :obj:`python:re.IGNORECASE` or :obj:`python:re.DOTALL` to control generation. Flags can be passed either in compiled regex or inside the pattern with a ``(?iLmsux)`` group. Some regular expressions are only partly supported - the underlying strategy checks local matching and relies on filtering to resolve context-dependent expressions. Using too many of these constructs may cause health-check errors as too many examples are filtered out. This mainly includes (positive or negative) lookahead and lookbehind groups. If you want the generated string to match the whole regex you should use boundary markers. So e.g. ``r"\A.\Z"`` will return a single character string, while ``"."`` will return any string, and ``r"\A.$"`` will return a single character optionally followed by a ``"\n"``. Alternatively, passing ``fullmatch=True`` will ensure that the whole string is a match, as if you had used the ``\A`` and ``\Z`` markers. The ``alphabet=`` argument constrains the characters in the generated string, as for :func:`text`, and is only supported for unicode strings. Examples from this strategy shrink towards shorter strings and lower character values, with exact behaviour that may depend on the pattern. """ check_type((str, bytes, re.Pattern), regex, "regex") check_type(bool, fullmatch, "fullmatch") pattern = regex.pattern if isinstance(regex, re.Pattern) else regex if alphabet is not None: check_type((str, SearchStrategy), alphabet, "alphabet") if not isinstance(pattern, str): raise InvalidArgument("alphabet= is not supported for bytestrings") alphabet = OneCharStringStrategy.from_alphabet(alphabet) elif isinstance(pattern, str): alphabet = characters(codec="utf-8") # TODO: We would like to move this to the top level, but pending some major # refactoring it's hard to do without creating circular imports. from hypothesis.strategies._internal.regex import regex_strategy return regex_strategy(regex, fullmatch, alphabet=alphabet) @cacheable @defines_strategy(force_reusable_values=True) def binary( *, min_size: int = 0, max_size: int | None = None, ) -> SearchStrategy[bytes]: """Generates :class:`python:bytes`. The generated :class:`python:bytes` will have a length of at least ``min_size`` and at most ``max_size``. If ``max_size`` is None there is no upper limit. Examples from this strategy shrink towards smaller strings and lower byte values. """ check_valid_sizes(min_size, max_size) return BytesStrategy(min_size, max_size) @cacheable @defines_strategy() def randoms( *, note_method_calls: bool = False, use_true_random: bool = False, ) -> SearchStrategy[random.Random]: """Generates instances of ``random.Random``. The generated Random instances are of a special HypothesisRandom subclass. - If ``note_method_calls`` is set to ``True``, Hypothesis will print the randomly drawn values in any falsifying test case. This can be helpful for debugging the behaviour of randomized algorithms. - If ``use_true_random`` is set to ``True`` then values will be drawn from their usual distribution, otherwise they will actually be Hypothesis generated values (and will be shrunk accordingly for any failing test case). Setting ``use_true_random=False`` will tend to expose bugs that would occur with very low probability when it is set to True, and this flag should only be set to True when your code relies on the distribution of values for correctness. For managing global state, see the :func:`~hypothesis.strategies.random_module` strategy and :func:`~hypothesis.register_random` function. """ check_type(bool, note_method_calls, "note_method_calls") check_type(bool, use_true_random, "use_true_random") from hypothesis.strategies._internal.random import RandomStrategy return RandomStrategy( use_true_random=use_true_random, note_method_calls=note_method_calls ) class RandomSeeder: def __init__(self, seed): self.seed = seed def __repr__(self): return f"RandomSeeder({self.seed!r})" class RandomModule(SearchStrategy): def do_draw(self, data: ConjectureData) -> RandomSeeder: # It would be unsafe to do run this method more than once per test case, # because cleanup() runs tasks in FIFO order (at time of writing!). # Fortunately, the random_module() strategy wraps us in shared(), so # it's cached for all but the first of any number of calls. seed = data.draw(integers(0, 2**32 - 1)) seed_all, restore_all = get_seeder_and_restorer(seed) seed_all() cleanup(restore_all) return RandomSeeder(seed) @cacheable @defines_strategy() def random_module() -> SearchStrategy[RandomSeeder]: """Hypothesis always seeds global PRNGs before running a test, and restores the previous state afterwards. If having a fixed seed would unacceptably weaken your tests, and you cannot use a ``random.Random`` instance provided by :func:`~hypothesis.strategies.randoms`, this strategy calls :func:`python:random.seed` with an arbitrary integer and passes you an opaque object whose repr displays the seed value for debugging. If ``numpy.random`` is available, that state is also managed, as is anything managed by :func:`hypothesis.register_random`. Examples from these strategy shrink to seeds closer to zero. """ return shared(RandomModule(), key="hypothesis.strategies.random_module()") class BuildsStrategy(SearchStrategy[Ex]): def __init__( self, target: Callable[..., Ex], args: tuple[SearchStrategy[Any], ...], kwargs: dict[str, SearchStrategy[Any]], ): super().__init__() self.target = target self.args = args self.kwargs = kwargs def calc_label(self) -> int: return combine_labels( self.class_label, calc_label_from_callable(self.target), *[strat.label for strat in self.args], *[calc_label_from_name(k) for k in self.kwargs], *[strat.label for strat in self.kwargs.values()], ) def do_draw(self, data: ConjectureData) -> Ex: context = current_build_context() arg_labels: ArgLabelsT = {} args = [] for i, s in enumerate(self.args): with context.track_arg_label(f"arg[{i}]") as arg_label: args.append(data.draw(s)) arg_labels |= arg_label kwargs = {} for k, v in self.kwargs.items(): with context.track_arg_label(k) as arg_label: kwargs[k] = data.draw(v) arg_labels |= arg_label try: obj = self.target(*args, **kwargs) except TypeError as err: if ( isinstance(self.target, type) and issubclass(self.target, enum.Enum) and not (self.args or self.kwargs) ): name = self.target.__module__ + "." + self.target.__qualname__ raise InvalidArgument( f"Calling {name} with no arguments raised an error - " f"try using sampled_from({name}) instead of builds({name})" ) from err if not (self.args or self.kwargs): from .types import is_generic_type if isinstance(self.target, NewType) or is_generic_type(self.target): raise InvalidArgument( f"Calling {self.target!r} with no arguments raised an " f"error - try using from_type({self.target!r}) instead " f"of builds({self.target!r})" ) from err if getattr(self.target, "__no_type_check__", None) is True: # Note: could use PEP-678 __notes__ here. Migrate over once we're # using an `exceptiongroup` backport with support for that. raise TypeError( "This might be because the @no_type_check decorator prevented " "Hypothesis from inferring a strategy for some required arguments." ) from err raise context.record_call( obj, self.target, args=args, kwargs=kwargs, arg_labels=arg_labels ) return obj def do_validate(self) -> None: tuples(*self.args).validate() fixed_dictionaries(self.kwargs).validate() def __repr__(self) -> str: bits = [get_pretty_function_description(self.target)] bits.extend(map(repr, self.args)) bits.extend(f"{k}={v!r}" for k, v in self.kwargs.items()) return f"builds({', '.join(bits)})" @cacheable @defines_strategy() def builds( target: Callable[..., Ex], /, *args: SearchStrategy[Any], **kwargs: SearchStrategy[Any] | EllipsisType, ) -> SearchStrategy[Ex]: """Generates values by drawing from ``args`` and ``kwargs`` and passing them to the callable (provided as the first positional argument) in the appropriate argument position. e.g. ``builds(target, integers(), flag=booleans())`` would draw an integer ``i`` and a boolean ``b`` and call ``target(i, flag=b)``. If the callable has type annotations, they will be used to infer a strategy for required arguments that were not passed to builds. You can also tell builds to infer a strategy for an optional argument by passing ``...`` (:obj:`python:Ellipsis`) as a keyword argument to builds, instead of a strategy for that argument to the callable. If the callable is a class defined with :pypi:`attrs`, missing required arguments will be inferred from the attribute on a best-effort basis, e.g. by checking :ref:`attrs standard validators `. Dataclasses are handled natively by the inference from type hints. Examples from this strategy shrink by shrinking the argument values to the callable. """ if not callable(target): from hypothesis.strategies._internal.types import is_a_union # before 3.14, unions were callable, so it got an error message in # BuildsStrategy.do_draw. In 3.14+, unions are not callable, so # we error earlier here instead. suggestion = ( f" Try using from_type({target}) instead?" if is_a_union(target) else "" ) raise InvalidArgument( "The first positional argument to builds() must be a callable " f"target to construct.{suggestion}" ) if ... in args: # type: ignore # we only annotated the allowed types # Avoid an implementation nightmare juggling tuples and worse things raise InvalidArgument( "... was passed as a positional argument to " "builds(), but is only allowed as a keyword arg" ) required = required_args(target, args, kwargs) to_infer = {k for k, v in kwargs.items() if v is ...} if required or to_infer: if ( isinstance(target, type) and (attr := sys.modules.get("attr")) is not None and attr.has(target) ): # pragma: no cover # covered by our attrs tests in check-niche # Use our custom introspection for attrs classes from hypothesis.strategies._internal.attrs import from_attrs return from_attrs(target, args, kwargs, required | to_infer) # Otherwise, try using type hints hints = get_type_hints(target) if to_infer - set(hints): badargs = ", ".join(sorted(to_infer - set(hints))) raise InvalidArgument( f"passed ... for {badargs}, but we cannot infer a strategy " "because these arguments have no type annotation" ) infer_for = {k: v for k, v in hints.items() if k in (required | to_infer)} if infer_for: from hypothesis.strategies._internal.types import _global_type_lookup for kw, t in infer_for.items(): if t in _global_type_lookup: kwargs[kw] = from_type(t) else: # We defer resolution of these type annotations so that the obvious # approach to registering recursive types just works. I.e., # if we're inside `register_type_strategy(cls, builds(cls, ...))` # and `...` contains recursion on `cls`. See # https://github.com/HypothesisWorks/hypothesis/issues/3026 kwargs[kw] = deferred(lambda t=t: from_type(t)) # type: ignore # validated by handling all EllipsisType in the to_infer case kwargs = cast(dict[str, SearchStrategy], kwargs) return BuildsStrategy(target, args, kwargs) @cacheable @defines_strategy(eager=True) def from_type(thing: type[T]) -> SearchStrategy[T]: """Looks up the appropriate search strategy for the given type. |st.from_type| is used internally to fill in missing arguments to |st.builds| and can be used interactively to explore what strategies are available or to debug type resolution. You can use |st.register_type_strategy| to handle your custom types, or to globally redefine certain strategies - for example excluding NaN from floats, or use timezone-aware instead of naive time and datetime strategies. |st.from_type| looks up a strategy in the following order: 1. If ``thing`` is in the default lookup mapping or user-registered lookup, return the corresponding strategy. The default lookup covers all types with Hypothesis strategies, including extras where possible. 2. If ``thing`` is from the :mod:`python:typing` module, return the corresponding strategy (special logic). 3. If ``thing`` has one or more subtypes in the merged lookup, return the union of the strategies for those types that are not subtypes of other elements in the lookup. 4. Finally, if ``thing`` has type annotations for all required arguments, and is not an abstract class, it is resolved via |st.builds|. 5. Because :mod:`abstract types ` cannot be instantiated, we treat abstract types as the union of their concrete subclasses. Note that this lookup works via inheritance but not via :obj:`~python:abc.ABCMeta.register`, so you may still need to use |st.register_type_strategy|. There is a valuable recipe for leveraging |st.from_type| to generate "everything except" values from a specified type. I.e. .. code-block:: python def everything_except(excluded_types): return ( from_type(type) .flatmap(from_type) .filter(lambda x: not isinstance(x, excluded_types)) ) For example, ``everything_except(int)`` returns a strategy that can generate anything that |st.from_type| can ever generate, except for instances of |int|, and excluding instances of types added via |st.register_type_strategy|. This is useful when writing tests which check that invalid input is rejected in a certain way. """ try: with warnings.catch_warnings(): warnings.simplefilter("error") return _from_type(thing) except Exception: return _from_type_deferred(thing) def _from_type_deferred(thing: type[Ex]) -> SearchStrategy[Ex]: # This tricky little dance is because we want to show the repr of the actual # underlying strategy wherever possible, as a form of user education, but # would prefer to fall back to the default "from_type(...)" repr instead of # "deferred(...)" for recursive types or invalid arguments. try: thing_repr = nicerepr(thing) if hasattr(thing, "__module__"): module_prefix = f"{thing.__module__}." if not thing_repr.startswith(module_prefix): thing_repr = module_prefix + thing_repr repr_ = f"from_type({thing_repr})" except Exception: # pragma: no cover repr_ = None return LazyStrategy( lambda thing: deferred(lambda: _from_type(thing)), (thing,), {}, force_repr=repr_, ) _recurse_guard: ContextVar = ContextVar("recurse_guard") def _from_type(thing: type[Ex]) -> SearchStrategy[Ex]: # TODO: We would like to move this to the top level, but pending some major # refactoring it's hard to do without creating circular imports. from hypothesis.strategies._internal import types def as_strategy(strat_or_callable, thing): # User-provided strategies need some validation, and callables even more # of it. We do this in three places, hence the helper function if not isinstance(strat_or_callable, SearchStrategy): assert callable(strat_or_callable) # Validated in register_type_strategy strategy = strat_or_callable(thing) else: strategy = strat_or_callable if strategy is NotImplemented: return NotImplemented if not isinstance(strategy, SearchStrategy): raise ResolutionFailed( f"Error: {thing} was registered for {nicerepr(strat_or_callable)}, " f"but returned non-strategy {strategy!r}" ) if strategy.is_empty: raise ResolutionFailed(f"Error: {thing!r} resolved to an empty strategy") return strategy def from_type_guarded(thing): """Returns the result of producer, or ... if recursion on thing is encountered""" try: recurse_guard = _recurse_guard.get() except LookupError: # We can't simply define the contextvar with default=[], as the # default object would be shared across contexts _recurse_guard.set(recurse_guard := []) if thing in recurse_guard: raise RewindRecursive(thing) recurse_guard.append(thing) try: return _from_type(thing) except RewindRecursive as rr: if rr.target != thing: raise return ... # defer resolution finally: recurse_guard.pop() # Let registered extra modules handle their own recognized types first, before # e.g. Unions are resolved try: known = thing in types._global_type_lookup except TypeError: # thing is not always hashable! pass else: if not known: for module, resolver in types._global_extra_lookup.items(): if module in sys.modules: strat = resolver(thing) if strat is not None: return strat if isinstance(thing, NewType): # Check if we have an explicitly registered strategy for this thing, # resolve it so, and otherwise resolve as for the base type. if thing in types._global_type_lookup: strategy = as_strategy(types._global_type_lookup[thing], thing) if strategy is not NotImplemented: return strategy return _from_type(thing.__supertype__) if types.is_a_type_alias_type(thing): # pragma: no cover # covered by 3.12+ tests if thing in types._global_type_lookup: strategy = as_strategy(types._global_type_lookup[thing], thing) if strategy is not NotImplemented: return strategy return _from_type(thing.__value__) # type: ignore if types.is_a_type_alias_type(origin := get_origin(thing)): # pragma: no cover # Handle parametrized type aliases like `type A[T] = list[T]; thing = A[int]`. # In this case, `thing` is a GenericAlias whose origin is a TypeAliasType. # # covered by 3.12+ tests. if origin in types._global_type_lookup: strategy = as_strategy(types._global_type_lookup[origin], thing) if strategy is not NotImplemented: return strategy return _from_type(types.evaluate_type_alias_type(thing)) if types.is_a_union(thing): args = sorted(thing.__args__, key=types.type_sorting_key) # type: ignore return one_of([_from_type(t) for t in args]) if thing in types.LiteralStringTypes: # pragma: no cover # We can't really cover this because it needs either # typing-extensions or python3.11+ typing. # `LiteralString` from runtime's point of view is just a string. # Fallback to regular text. return text() # type: ignore # We also have a special case for TypeVars. # They are represented as instances like `~T` when they come here. # We need to work with their type instead. if isinstance(thing, TypeVar) and type(thing) in types._global_type_lookup: strategy = as_strategy(types._global_type_lookup[type(thing)], thing) if strategy is not NotImplemented: return strategy if not types.is_a_type(thing): if isinstance(thing, str): # See https://github.com/HypothesisWorks/hypothesis/issues/3016 raise InvalidArgument( f"Got {thing!r} as a type annotation, but the forward-reference " "could not be resolved from a string to a type. Consider using " "`from __future__ import annotations` instead of forward-reference " "strings." ) raise InvalidArgument(f"{thing=} must be a type") # pragma: no cover if thing in types.NON_RUNTIME_TYPES: # Some code like `st.from_type(TypeAlias)` does not make sense. # Because there are types in python that do not exist in runtime. raise InvalidArgument( f"Could not resolve {thing!r} to a strategy, " f"because there is no such thing as a runtime instance of {thing!r}" ) # Now that we know `thing` is a type, the first step is to check for an # explicitly registered strategy. This is the best (and hopefully most # common) way to resolve a type to a strategy. Note that the value in the # lookup may be a strategy or a function from type -> strategy; and we # convert empty results into an explicit error. try: if thing in types._global_type_lookup: strategy = as_strategy(types._global_type_lookup[thing], thing) if strategy is not NotImplemented: return strategy elif ( isinstance(thing, GenericAlias) and (origin := get_origin(thing)) in types._global_type_lookup ): strategy = as_strategy(types._global_type_lookup[origin], thing) if strategy is not NotImplemented: return strategy except TypeError: # pragma: no cover # This was originally due to a bizarre divergence in behaviour on Python 3.9.0: # typing.Callable[[], foo] has __args__ = (foo,) but collections.abc.Callable # has __args__ = ([], foo); and as a result is non-hashable. # We've kept it because we turn out to have more type errors from... somewhere. # FIXME: investigate that, maybe it should be fixed more precisely? pass if (hasattr(typing, "_TypedDictMeta") and type(thing) is typing._TypedDictMeta) or ( hasattr(types.typing_extensions, "_TypedDictMeta") # type: ignore and type(thing) is types.typing_extensions._TypedDictMeta # type: ignore ): # pragma: no cover def _get_annotation_arg(key, annotation_type): try: return get_args(annotation_type)[0] except IndexError: raise InvalidArgument( f"`{key}: {annotation_type.__name__}` is not a valid type annotation" ) from None # Taken from `Lib/typing.py` and modified: def _get_typeddict_qualifiers(key, annotation_type): qualifiers = [] annotations = [] while True: annotation_origin = types.extended_get_origin(annotation_type) if annotation_origin is Annotated: if annotation_args := get_args(annotation_type): annotation_type = annotation_args[0] annotations.extend(annotation_args[1:]) else: break elif annotation_origin in types.RequiredTypes: qualifiers.append(types.RequiredTypes) annotation_type = _get_annotation_arg(key, annotation_type) elif annotation_origin in types.NotRequiredTypes: qualifiers.append(types.NotRequiredTypes) annotation_type = _get_annotation_arg(key, annotation_type) elif annotation_origin in types.ReadOnlyTypes: qualifiers.append(types.ReadOnlyTypes) annotation_type = _get_annotation_arg(key, annotation_type) else: break if annotations: annotation_type = Annotated[(annotation_type, *annotations)] return set(qualifiers), annotation_type # The __optional_keys__ attribute may or may not be present, but if there's no # way to tell and we just have to assume that everything is required. # See https://github.com/python/cpython/pull/17214 for details. optional = set(getattr(thing, "__optional_keys__", ())) required = set( getattr(thing, "__required_keys__", get_type_hints(thing).keys()) ) anns = {} for k, v in get_type_hints(thing).items(): qualifiers, v = _get_typeddict_qualifiers(k, v) # We ignore `ReadOnly` type for now, only unwrap it. if types.RequiredTypes in qualifiers: optional.discard(k) required.add(k) if types.NotRequiredTypes in qualifiers: optional.add(k) required.discard(k) anns[k] = from_type_guarded(v) if anns[k] is ...: anns[k] = _from_type_deferred(v) if not required.isdisjoint(optional): # pragma: no cover # It is impossible to cover, because `typing.py` or `typing-extensions` # won't allow creating incorrect TypedDicts, # this is just a sanity check from our side. raise InvalidArgument( f"Required keys overlap with optional keys in a TypedDict:" f" {required=}, {optional=}" ) if ( (not anns) and thing.__annotations__ and ".." in getattr(thing, "__qualname__", "") ): raise InvalidArgument("Failed to retrieve type annotations for local type") return fixed_dictionaries( # type: ignore mapping={k: v for k, v in anns.items() if k in required}, optional={k: v for k, v in anns.items() if k in optional}, ) # If there's no explicitly registered strategy, maybe a subtype of thing # is registered - if so, we can resolve it to the subclass strategy. # We'll start by checking if thing is from the typing module, # because there are several special cases that don't play well with # subclass and instance checks. if ( isinstance(thing, types.typing_root_type) or (isinstance(get_origin(thing), type) and get_args(thing)) or isinstance(thing, typing.ForwardRef) ): return types.from_typing_type(thing) # If it's not from the typing module, we get all registered types that are # a subclass of `thing` and are not themselves a subtype of any other such # type. For example, `Number -> integers() | floats()`, but bools() is # not included because bool is a subclass of int as well as Number. strategies = [ s for s in ( as_strategy(v, thing) for k, v in sorted(types._global_type_lookup.items(), key=repr) if isinstance(k, type) and issubclass(k, thing) and sum(types.try_issubclass(k, typ) for typ in types._global_type_lookup) == 1 ) if s is not NotImplemented ] if any(not s.is_empty for s in strategies): return one_of(strategies) # If we don't have a strategy registered for this type or any subtype, we # may be able to fall back on type annotations. if issubclass(thing, enum.Enum): return sampled_from(thing) # Finally, try to build an instance by calling the type object. Unlike builds(), # this block *does* try to infer strategies for arguments with default values. # That's because of the semantic different; builds() -> "call this with ..." # so we only infer when *not* doing so would be an error; from_type() -> "give # me arbitrary instances" so the greater variety is acceptable. # And if it's *too* varied, express your opinions with register_type_strategy() if not isabstract(thing): # If we know that builds(thing) will fail, give a better error message required = required_args(thing) if required and not ( required.issubset(get_type_hints(thing)) or ((attr := sys.modules.get("attr")) is not None and attr.has(thing)) or is_typed_named_tuple(thing) # weird enough that we have a specific check ): raise ResolutionFailed( f"Could not resolve {thing!r} to a strategy; consider " "using register_type_strategy" ) try: hints = get_type_hints(thing) params = get_signature(thing).parameters except Exception: params = {} # type: ignore posonly_args = [] kwargs = {} for k, p in params.items(): if ( p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY) and k in hints and k != "return" ): ps = from_type_guarded(hints[k]) if p.default is not Parameter.empty and ps is not ...: ps = just(p.default) | ps if p.kind is Parameter.POSITIONAL_ONLY: # builds() doesn't infer strategies for positional args, so: if ps is ...: # pragma: no cover # rather fiddly to test if p.default is Parameter.empty: raise ResolutionFailed( f"Could not resolve {thing!r} to a strategy; " "consider using register_type_strategy" ) ps = just(p.default) posonly_args.append(ps) else: kwargs[k] = ps if ( params and not (posonly_args or kwargs) and not issubclass(thing, BaseException) ): from_type_repr = repr_call(from_type, (thing,), {}) builds_repr = repr_call(builds, (thing,), {}) warnings.warn( f"{from_type_repr} resolved to {builds_repr}, because we could not " "find any (non-varargs) arguments. Use st.register_type_strategy() " "to resolve to a strategy which can generate more than one value, " "or to silence this warning.", SmallSearchSpaceWarning, stacklevel=2, ) return builds(thing, *posonly_args, **kwargs) # And if it's an abstract type, we'll resolve to a union of subclasses instead. subclasses = thing.__subclasses__() if not subclasses: raise ResolutionFailed( f"Could not resolve {thing!r} to a strategy, because it is an abstract " "type without any subclasses. Consider using register_type_strategy" ) subclass_strategies: SearchStrategy = nothing() for sc in subclasses: try: subclass_strategies |= _from_type(sc) except Exception: pass if subclass_strategies.is_empty: # We're unable to resolve subclasses now, but we might be able to later - # so we'll just go back to the mixed distribution. return sampled_from(subclasses).flatmap(_from_type) return subclass_strategies @cacheable @defines_strategy(force_reusable_values=True) def fractions( min_value: Real | str | None = None, max_value: Real | str | None = None, *, max_denominator: int | None = None, ) -> SearchStrategy[Fraction]: """Returns a strategy which generates Fractions. If ``min_value`` is not None then all generated values are no less than ``min_value``. If ``max_value`` is not None then all generated values are no greater than ``max_value``. ``min_value`` and ``max_value`` may be anything accepted by the :class:`~fractions.Fraction` constructor. If ``max_denominator`` is not None then the denominator of any generated values is no greater than ``max_denominator``. Note that ``max_denominator`` must be None or a positive integer. Examples from this strategy shrink towards smaller denominators, then closer to zero. """ min_value = try_convert(Fraction, min_value, "min_value") max_value = try_convert(Fraction, max_value, "max_value") # These assertions tell Mypy what happened in try_convert assert min_value is None or isinstance(min_value, Fraction) assert max_value is None or isinstance(max_value, Fraction) check_valid_interval(min_value, max_value, "min_value", "max_value") check_valid_integer(max_denominator, "max_denominator") if max_denominator is not None: if max_denominator < 1: raise InvalidArgument(f"{max_denominator=} must be >= 1") if min_value is not None and min_value.denominator > max_denominator: raise InvalidArgument( f"The {min_value=} has a denominator greater than the " f"{max_denominator=}" ) if max_value is not None and max_value.denominator > max_denominator: raise InvalidArgument( f"The {max_value=} has a denominator greater than the " f"{max_denominator=}" ) if min_value is not None and min_value == max_value: return just(min_value) def dm_func(denom): """Take denom, construct numerator strategy, and build fraction.""" # Four cases of algebra to get integer bounds and scale factor. min_num, max_num = None, None if max_value is None and min_value is None: pass elif min_value is None: max_num = denom * max_value.numerator denom *= max_value.denominator elif max_value is None: min_num = denom * min_value.numerator denom *= min_value.denominator else: low = min_value.numerator * max_value.denominator high = max_value.numerator * min_value.denominator scale = min_value.denominator * max_value.denominator # After calculating our integer bounds and scale factor, we remove # the gcd to avoid drawing more bytes for the example than needed. # Note that `div` can be at most equal to `scale`. div = math.gcd(scale, math.gcd(low, high)) min_num = denom * low // div max_num = denom * high // div denom *= scale // div return builds( Fraction, integers(min_value=min_num, max_value=max_num), just(denom) ) if max_denominator is None: return integers(min_value=1).flatmap(dm_func) return ( integers(1, max_denominator) .flatmap(dm_func) .map(lambda f: f.limit_denominator(max_denominator)) ) def _as_finite_decimal( value: Real | str | None, name: str, allow_infinity: bool | None, places: int | None ) -> Decimal | None: """Convert decimal bounds to decimals, carefully.""" assert name in ("min_value", "max_value") if value is None: return None old = value if isinstance(value, Fraction): value = Context(prec=places).divide(value.numerator, value.denominator) if old != value: raise InvalidArgument( f"{old!r} cannot be exactly represented as a decimal with {places=}" ) if not isinstance(value, Decimal): with localcontext(Context()): # ensure that default traps are enabled value = try_convert(Decimal, value, name) assert isinstance(value, Decimal) if value.is_nan(): raise InvalidArgument(f"Invalid {name}={value!r}") # If you are reading this conditional, I am so sorry. I did my best. finitude_old = value if isinstance(old, str) else old if math.isfinite(finitude_old) != math.isfinite(value) or ( value.is_finite() and Fraction(str(old)) != Fraction(str(value)) ): note_deprecation( f"{old!r} cannot be exactly represented as a decimal with {places=}", since="2025-11-02", has_codemod=False, stacklevel=1, ) if value.is_finite(): return value assert value.is_infinite() if (value < 0 if "min" in name else value > 0) and allow_infinity is not False: return None raise InvalidArgument(f"{allow_infinity=}, but {name}={value!r}") @cacheable @defines_strategy(force_reusable_values=True) def decimals( min_value: Real | str | None = None, max_value: Real | str | None = None, *, allow_nan: bool | None = None, allow_infinity: bool | None = None, places: int | None = None, ) -> SearchStrategy[Decimal]: """Generates instances of :class:`python:decimal.Decimal`, which may be: - A finite rational number, between ``min_value`` and ``max_value``. - Not a Number, if ``allow_nan`` is True. None means "allow NaN, unless ``min_value`` and ``max_value`` are not None". - Positive or negative infinity, if ``max_value`` and ``min_value`` respectively are None, and ``allow_infinity`` is not False. None means "allow infinity, unless excluded by the min and max values". Note that where floats have one ``NaN`` value, Decimals have four: signed, and either *quiet* or *signalling*. See `the decimal module docs `_ for more information on special values. If ``places`` is not None, all finite values drawn from the strategy will have that number of digits after the decimal place. Examples from this strategy do not have a well defined shrink order but try to maximize human readability when shrinking. """ # Convert min_value and max_value to Decimal values, and validate args check_valid_integer(places, "places") if places is not None and places < 0: raise InvalidArgument(f"{places=} may not be negative") min_value = _as_finite_decimal(min_value, "min_value", allow_infinity, places) max_value = _as_finite_decimal(max_value, "max_value", allow_infinity, places) check_valid_interval(min_value, max_value, "min_value", "max_value") if allow_infinity and (None not in (min_value, max_value)): raise InvalidArgument("Cannot allow infinity between finite bounds") # Set up a strategy for finite decimals. Note that both floating and # fixed-point decimals require careful handling to remain isolated from # any external precision context - in short, we always work out the # required precision for lossless operation and use context methods. if places is not None: # Fixed-point decimals are basically integers with a scale factor def ctx(val): """Return a context in which this value is lossless.""" precision = ceil(math.log10(abs(val) or 1)) + places + 1 return Context(prec=max([precision, 1])) def int_to_decimal(val): context = ctx(val) return context.quantize(context.multiply(val, factor), factor) factor = Decimal(10) ** -places min_num, max_num = None, None if min_value is not None: min_num = ceil(ctx(min_value).divide(min_value, factor)) if max_value is not None: max_num = floor(ctx(max_value).divide(max_value, factor)) if min_num is not None and max_num is not None and min_num > max_num: raise InvalidArgument( f"There are no decimals with {places} places between " f"{min_value=} and {max_value=}" ) strat = integers(min_num, max_num).map(int_to_decimal) else: # Otherwise, they're like fractions featuring a power of ten def fraction_to_decimal(val): precision = ( ceil(math.log10(abs(val.numerator) or 1) + math.log10(val.denominator)) + 1 ) return Context(prec=precision or 1).divide( Decimal(val.numerator), val.denominator ) strat = fractions(min_value, max_value).map(fraction_to_decimal) # Compose with sampled_from for infinities and NaNs as appropriate special: list[Decimal] = [] if allow_infinity or (allow_infinity is None and max_value is None): special.append(Decimal("Infinity")) if allow_infinity or (allow_infinity is None and min_value is None): special.append(Decimal("-Infinity")) if allow_nan or (allow_nan is None and (None in (min_value, max_value))): special.extend(map(Decimal, ("NaN", "-NaN", "sNaN", "-sNaN"))) return strat | (sampled_from(special) if special else nothing()) @defines_strategy(eager=True) def recursive( base: SearchStrategy[Ex], extend: Callable[[SearchStrategy[Any]], SearchStrategy[T]], *, min_leaves: int | None = None, max_leaves: int = 100, ) -> SearchStrategy[T | Ex]: """base: A strategy to start from. extend: A function which takes a strategy and returns a new strategy. min_leaves: The minimum number of elements to be drawn from base on a given run. max_leaves: The maximum number of elements to be drawn from base on a given run. This returns a strategy ``S`` such that ``S = extend(base | S)``. That is, values may be drawn from base, or from any strategy reachable by mixing applications of | and extend. An example may clarify: ``recursive(booleans(), lists)`` would return a strategy that may return arbitrarily nested and mixed lists of booleans. So e.g. ``False``, ``[True]``, ``[False, []]``, and ``[[[[True]]]]`` are all valid values to be drawn from that strategy. Examples from this strategy shrink by trying to reduce the amount of recursion and by shrinking according to the shrinking behaviour of base and the result of extend. """ return RecursiveStrategy(base, extend, min_leaves, max_leaves) class PermutationStrategy(SearchStrategy): def __init__(self, values): super().__init__() self.values = values def do_draw(self, data): # Reversed Fisher-Yates shuffle: swap each element with itself or with # a later element. This shrinks i==j for each element, i.e. to no # change. We don't consider the last element as it's always a no-op. result = list(self.values) for i in range(len(result) - 1): j = data.draw_integer(i, len(result) - 1) result[i], result[j] = result[j], result[i] return result @defines_strategy() def permutations(values: Sequence[T]) -> SearchStrategy[list[T]]: """Return a strategy which returns permutations of the ordered collection ``values``. Examples from this strategy shrink by trying to become closer to the original order of values. """ values = check_sample(values, "permutations") if not values: return builds(list) return PermutationStrategy(values) class CompositeStrategy(SearchStrategy): def __init__(self, definition, args, kwargs): super().__init__() self.definition = definition self.args = args self.kwargs = kwargs def do_draw(self, data): return self.definition(data.draw, *self.args, **self.kwargs) def calc_label(self) -> int: return combine_labels( self.class_label, calc_label_from_callable(self.definition), ) class DrawFn(Protocol): """This type only exists so that you can write type hints for functions decorated with :func:`@composite `. .. code-block:: python def draw(strategy: SearchStrategy[Ex], label: object = None) -> Ex: ... @composite def list_and_index(draw: DrawFn) -> tuple[int, str]: i = draw(integers()) # type of `i` inferred as 'int' s = draw(text()) # type of `s` inferred as 'str' return i, s """ def __init__(self): raise TypeError("Protocols cannot be instantiated") # pragma: no cover # Protocol overrides our signature for __init__, # so we override it right back to make the docs look nice. __signature__: Signature = Signature(parameters=[]) # We define this as a callback protocol because a simple typing.Callable is # insufficient to fully represent the interface, due to the optional `label` # parameter. def __call__(self, strategy: SearchStrategy[Ex], label: object = None) -> Ex: raise NotImplementedError def _composite(f): # Wrapped below, using ParamSpec if available if isinstance(f, (classmethod, staticmethod)): special_method = type(f) f = f.__func__ else: special_method = None sig = get_signature(f) params = tuple(sig.parameters.values()) if not (params and "POSITIONAL" in params[0].kind.name): raise InvalidArgument( "Functions wrapped with composite must take at least one " "positional argument." ) if params[0].default is not sig.empty: raise InvalidArgument("A default value for initial argument will never be used") if not (f is typing._overload_dummy or is_first_param_referenced_in_function(f)): note_deprecation( "There is no reason to use @st.composite on a function which " "does not call the provided draw() function internally.", since="2022-07-17", has_codemod=False, ) if get_origin(sig.return_annotation) is SearchStrategy: ret_repr = repr(sig.return_annotation).replace("hypothesis.strategies.", "st.") warnings.warn( f"Return-type annotation is `{ret_repr}`, but the decorated " "function should return a value (not a strategy)", HypothesisWarning, stacklevel=3, ) if params[0].kind.name != "VAR_POSITIONAL": params = params[1:] newsig = sig.replace( parameters=params, return_annotation=( SearchStrategy if sig.return_annotation is sig.empty else SearchStrategy[sig.return_annotation] ), ) @defines_strategy() @define_function_signature(f.__name__, f.__doc__, newsig) def accept(*args, **kwargs): return CompositeStrategy(f, args, kwargs) accept.__module__ = f.__module__ accept.__signature__ = newsig if special_method is not None: return special_method(accept) return accept composite_doc = """ Defines a strategy that is built out of potentially arbitrarily many other strategies. @composite provides a callable ``draw`` as the first parameter to the decorated function, which can be used to dynamically draw a value from any strategy. For example: .. code-block:: python from hypothesis import strategies as st, given @st.composite def values(draw): n1 = draw(st.integers()) n2 = draw(st.integers(min_value=n1)) return (n1, n2) @given(values()) def f(value): (n1, n2) = value assert n1 <= n2 @composite cannot mix test code and generation code. If you need that, use |st.data|. If :func:`@composite ` is used to decorate a method or classmethod, the ``draw`` argument must come before ``self`` or ``cls``. While we therefore recommend writing strategies as standalone functions and using |st.register_type_strategy| to associate them with a class, methods are supported and the ``@composite`` decorator may be applied either before or after ``@classmethod`` or ``@staticmethod``. See :issue:`2578` and :pull:`2634` for more details. Examples from this strategy shrink by shrinking the output of each draw call. """ if typing.TYPE_CHECKING or ParamSpec is not None: P = ParamSpec("P") def composite( f: Callable[Concatenate[DrawFn, P], Ex], ) -> Callable[P, SearchStrategy[Ex]]: return _composite(f) else: # pragma: no cover @cacheable def composite(f: Callable[..., Ex]) -> Callable[..., SearchStrategy[Ex]]: return _composite(f) composite.__doc__ = composite_doc @defines_strategy(force_reusable_values=True) @cacheable def complex_numbers( *, min_magnitude: Real = 0, max_magnitude: Real | None = None, allow_infinity: bool | None = None, allow_nan: bool | None = None, allow_subnormal: bool = True, width: Literal[32, 64, 128] = 128, ) -> SearchStrategy[complex]: """Returns a strategy that generates :class:`~python:complex` numbers. This strategy draws complex numbers with constrained magnitudes. The ``min_magnitude`` and ``max_magnitude`` parameters should be non-negative :class:`~python:numbers.Real` numbers; a value of ``None`` corresponds an infinite upper bound. If ``min_magnitude`` is nonzero or ``max_magnitude`` is finite, it is an error to enable ``allow_nan``. If ``max_magnitude`` is finite, it is an error to enable ``allow_infinity``. ``allow_infinity``, ``allow_nan``, and ``allow_subnormal`` are applied to each part of the complex number separately, as for :func:`~hypothesis.strategies.floats`. The magnitude constraints are respected up to a relative error of (around) floating-point epsilon, due to implementation via the system ``sqrt`` function. The ``width`` argument specifies the maximum number of bits of precision required to represent the entire generated complex number. Valid values are 32, 64 or 128, which correspond to the real and imaginary components each having width 16, 32 or 64, respectively. Passing ``width=64`` will still use the builtin 128-bit :class:`~python:complex` class, but always for values which can be exactly represented as two 32-bit floats. Examples from this strategy shrink by shrinking their real and imaginary parts, as :func:`~hypothesis.strategies.floats`. If you need to generate complex numbers with particular real and imaginary parts or relationships between parts, consider using :func:`builds(complex, ...) ` or :func:`@composite ` respectively. """ check_valid_magnitude(min_magnitude, "min_magnitude") check_valid_magnitude(max_magnitude, "max_magnitude") check_valid_interval(min_magnitude, max_magnitude, "min_magnitude", "max_magnitude") if max_magnitude == math.inf: max_magnitude = None if allow_infinity is None: allow_infinity = bool(max_magnitude is None) elif allow_infinity and max_magnitude is not None: raise InvalidArgument(f"Cannot have {allow_infinity=} with {max_magnitude=}") if allow_nan is None: allow_nan = bool(min_magnitude == 0 and max_magnitude is None) elif allow_nan and not (min_magnitude == 0 and max_magnitude is None): raise InvalidArgument( f"Cannot have {allow_nan=}, {min_magnitude=}, {max_magnitude=}" ) check_type(bool, allow_subnormal, "allow_subnormal") if width not in (32, 64, 128): raise InvalidArgument( f"{width=}, but must be 32, 64 or 128 (other complex dtypes " "such as complex192 or complex256 are not supported)" # For numpy, these types would be supported (but not by CPython): # https://numpy.org/doc/stable/reference/arrays.scalars.html#complex-floating-point-types ) component_width = width // 2 allow_kw = { "allow_nan": allow_nan, "allow_infinity": allow_infinity, # If we have a nonzero normal min_magnitude and draw a zero imaginary part, # then allow_subnormal=True would be an error with the min_value to the floats() # strategy for the real part. We therefore replace True with None. "allow_subnormal": None if allow_subnormal else allow_subnormal, "width": component_width, } if min_magnitude == 0 and max_magnitude is None: # In this simple but common case, there are no constraints on the # magnitude and therefore no relationship between the real and # imaginary parts. return builds(complex, floats(**allow_kw), floats(**allow_kw)) # type: ignore @composite def constrained_complex(draw): # We downcast drawn floats to the desired (component) width so we # guarantee the resulting complex values are representable. Note # truncating the mantissa bits with float_of() cannot increase the # magnitude of a float, so we are guaranteed to stay within the allowed # range. See https://github.com/HypothesisWorks/hypothesis/issues/3573 # Draw the imaginary part, and determine the maximum real part given # this and the max_magnitude if max_magnitude is None: zi = draw(floats(**allow_kw)) rmax = None else: zi = draw( floats( -float_of(max_magnitude, component_width), float_of(max_magnitude, component_width), **allow_kw, ) ) rmax = float_of(cathetus(max_magnitude, zi), component_width) # Draw the real part from the allowed range given the imaginary part if min_magnitude == 0 or math.fabs(zi) >= min_magnitude: zr = draw(floats(None if rmax is None else -rmax, rmax, **allow_kw)) else: rmin = float_of(cathetus(min_magnitude, zi), component_width) zr = draw(floats(rmin, rmax, **allow_kw)) # Order of conditions carefully tuned so that for a given pair of # magnitude arguments, we always either draw or do not draw the bool # (crucial for good shrinking behaviour) but only invert when needed. if min_magnitude > 0 and draw(booleans()) and math.fabs(zi) <= min_magnitude: zr = -zr return complex(zr, zi) return constrained_complex() @defines_strategy(eager=True) def shared( base: SearchStrategy[Ex], *, key: Hashable | None = None, ) -> SearchStrategy[Ex]: """Returns a strategy that draws a single shared value per run, drawn from base. Any two shared instances with the same key will share the same value, otherwise the identity of this strategy will be used. That is: >>> s = integers() # or any other strategy >>> x = shared(s) >>> y = shared(s) In the above x and y may draw different (or potentially the same) values. In the following they will always draw the same: >>> x = shared(s, key="hi") >>> y = shared(s, key="hi") Examples from this strategy shrink as per their base strategy. """ return SharedStrategy(base, key) @composite def _maybe_nil_uuids(draw, uuid): # Equivalent to `random_uuids | just(...)`, with a stronger bias to the former. if draw(data()).conjecture_data.draw_boolean(1 / 64): return UUID("00000000-0000-0000-0000-000000000000") return uuid @cacheable @defines_strategy(force_reusable_values=True) def uuids( *, version: Literal[1, 2, 3, 4, 5] | None = None, allow_nil: bool = False ) -> SearchStrategy[UUID]: """Returns a strategy that generates :class:`UUIDs `. If the optional version argument is given, value is passed through to :class:`~python:uuid.UUID` and only UUIDs of that version will be generated. If ``allow_nil`` is True, generate the nil UUID much more often. Otherwise, all returned values from this will be unique, so e.g. if you do ``lists(uuids())`` the resulting list will never contain duplicates. Examples from this strategy don't have any meaningful shrink order. """ check_type(bool, allow_nil, "allow_nil") if version not in (None, 1, 2, 3, 4, 5): raise InvalidArgument( f"{version=}, but version must be in " "(None, 1, 2, 3, 4, 5) to pass to the uuid.UUID constructor." ) random_uuids = shared( randoms(use_true_random=True), key="hypothesis.strategies.uuids.generator" ).map(lambda r: UUID(version=version, int=r.getrandbits(128))) if allow_nil: if version is not None: raise InvalidArgument("The nil UUID is not of any version") return random_uuids.flatmap(_maybe_nil_uuids) return random_uuids class RunnerStrategy(SearchStrategy): def __init__(self, default): super().__init__() self.default = default def do_draw(self, data): if data.hypothesis_runner is not_set: if self.default is not_set: raise InvalidArgument( "Cannot use runner() strategy with no " "associated runner or explicit default." ) return self.default return data.hypothesis_runner @defines_strategy(force_reusable_values=True) def runner(*, default: Any = not_set) -> SearchStrategy[Any]: """A strategy for getting "the current test runner", whatever that may be. The exact meaning depends on the entry point, but it will usually be the associated 'self' value for it. If you are using this in a rule for stateful testing, this strategy will return the instance of the :class:`~hypothesis.stateful.RuleBasedStateMachine` that the rule is running for. If there is no current test runner and a default is provided, return that default. If no default is provided, raises InvalidArgument. Examples from this strategy do not shrink (because there is only one). """ return RunnerStrategy(default) class DataObject: """This type only exists so that you can write type hints for tests using the :func:`~hypothesis.strategies.data` strategy. Do not use it directly! """ # Note that "only exists" here really means "is only exported to users", # but we want to treat it as "semi-stable", not document it as "public API". def __init__(self, data: ConjectureData) -> None: self.count = 0 self.conjecture_data = data __signature__ = Signature() # hide internals from Sphinx introspection def __repr__(self) -> str: return "data(...)" def draw(self, strategy: SearchStrategy[Ex], label: Any = None) -> Ex: """Like :obj:`~hypothesis.strategies.DrawFn`.""" check_strategy(strategy, "strategy") self.count += 1 desc = f"Draw {self.count}{'' if label is None else f' ({label})'}" with deprecate_random_in_strategy("{}from {!r}", desc, strategy): result = self.conjecture_data.draw(strategy, observe_as=f"generate:{desc}") # optimization to avoid needless printer.pretty if should_note(): printer = RepresentationPrinter(context=current_build_context()) printer.text(f"{desc}: ") if self.conjecture_data.provider.avoid_realization: printer.text("") else: printer.pretty(result) note(printer.getvalue()) return result class DataStrategy(SearchStrategy): def do_draw(self, data): if data._shared_data_strategy is None: data._shared_data_strategy = DataObject(data) return data._shared_data_strategy def __repr__(self) -> str: return "data()" def map(self, f): self.__not_a_first_class_strategy("map") def filter(self, condition: Callable[[Ex], Any]) -> NoReturn: self.__not_a_first_class_strategy("filter") def flatmap(self, f): self.__not_a_first_class_strategy("flatmap") def example(self) -> NoReturn: self.__not_a_first_class_strategy("example") def __not_a_first_class_strategy(self, name: str) -> NoReturn: raise InvalidArgument( f"Cannot call {name} on a DataStrategy. You should probably " "be using @composite for whatever it is you're trying to do." ) @cacheable @defines_strategy(eager=True) def data() -> SearchStrategy[DataObject]: """ Provides an object ``data`` with a ``data.draw`` function which acts like the ``draw`` callable provided by |st.composite|, in that it can be used to dynamically draw values from strategies. |st.data| is more powerful than |st.composite|, because it allows you to mix generation and test code. Here's an example of dynamically generating values using |st.data|: .. code-block:: python from hypothesis import strategies as st, given @given(st.data()) def test_values(data): n1 = data.draw(st.integers()) n2 = data.draw(st.integers(min_value=n1)) assert n1 + 1 <= n2 If the test fails, each draw will be printed with the falsifying example. e.g. the above is wrong (it has a boundary condition error), so will print: .. code-block:: pycon Falsifying example: test_values(data=data(...)) Draw 1: 0 Draw 2: 0 Optionally, you can provide a label to identify values generated by each call to ``data.draw()``. These labels can be used to identify values in the output of a falsifying example. For instance: .. code-block:: python @given(st.data()) def test_draw_sequentially(data): x = data.draw(st.integers(), label="First number") y = data.draw(st.integers(min_value=x), label="Second number") assert x < y will produce: .. code-block:: pycon Falsifying example: test_draw_sequentially(data=data(...)) Draw 1 (First number): 0 Draw 2 (Second number): 0 Examples from this strategy shrink by shrinking the output of each draw call. """ return DataStrategy() if sys.version_info < (3, 12): # TypeAliasType is new in 3.12 RegisterTypeT: TypeAlias = type[Ex] else: # pragma: no cover # covered by test_mypy.py from typing import TypeAliasType # see https://github.com/HypothesisWorks/hypothesis/issues/4410 RegisterTypeT: TypeAlias = type[Ex] | TypeAliasType def register_type_strategy( custom_type: RegisterTypeT, strategy: SearchStrategy[Ex] | Callable[[type[Ex]], SearchStrategy[Ex]], ) -> None: """Add an entry to the global type-to-strategy lookup. This lookup is used in :func:`~hypothesis.strategies.builds` and |@given|. :func:`~hypothesis.strategies.builds` will be used automatically for classes with type annotations on ``__init__`` , so you only need to register a strategy if one or more arguments need to be more tightly defined than their type-based default, or if you want to supply a strategy for an argument with a default value. ``strategy`` may be a search strategy, or a function that takes a type and returns a strategy (useful for generic types). The function may return :data:`NotImplemented` to conditionally not provide a strategy for the type (the type will still be resolved by other methods, if possible, as if the function was not registered). Note that you may not register a parametrised generic type (such as ``MyCollection[int]``) directly, because the resolution logic does not handle this case correctly. Instead, you may register a *function* for ``MyCollection`` and `inspect the type parameters within that function `__. """ # TODO: We would like to move this to the top level, but pending some major # refactoring it's hard to do without creating circular imports. from hypothesis.strategies._internal import types if not types.is_a_type(custom_type): raise InvalidArgument(f"{custom_type=} must be a type") if custom_type in types.NON_RUNTIME_TYPES: raise InvalidArgument( f"{custom_type=} is not allowed to be registered, " f"because there is no such thing as a runtime instance of {custom_type!r}" ) if not (isinstance(strategy, SearchStrategy) or callable(strategy)): raise InvalidArgument( f"{strategy=} must be a SearchStrategy, or a function that takes " "a generic type and returns a specific SearchStrategy" ) if isinstance(strategy, SearchStrategy): with warnings.catch_warnings(): warnings.simplefilter("error", HypothesisSideeffectWarning) # Calling is_empty forces materialization of lazy strategies. If this is done at import # time, lazy strategies will warn about it; here, we force that warning to raise to # avoid the materialization. Ideally, we'd just check if the strategy is lazy, but the # lazy strategy may be wrapped underneath another strategy so that's complicated. try: if strategy.is_empty: raise InvalidArgument(f"{strategy=} must not be empty") except HypothesisSideeffectWarning: # pragma: no cover pass if types.has_type_arguments(custom_type): raise InvalidArgument( f"Cannot register generic type {custom_type!r}, because it has type " "arguments which would not be handled. Instead, register a function " f"for {get_origin(custom_type)!r} which can inspect specific type " "objects and return a strategy." ) if ( "pydantic.generics" in sys.modules and isinstance(custom_type, type) and issubclass(custom_type, sys.modules["pydantic.generics"].GenericModel) and not re.search(r"[A-Za-z_]+\[.+\]", repr(custom_type)) and callable(strategy) ): # pragma: no cover # See https://github.com/HypothesisWorks/hypothesis/issues/2940 raise InvalidArgument( f"Cannot register a function for {custom_type!r}, because parametrized " "`pydantic.generics.GenericModel` subclasses aren't actually generic " "types at runtime. In this case, you should register a strategy " "directly for each parametrized form that you anticipate using." ) types._global_type_lookup[custom_type] = strategy from_type.__clear_cache() # type: ignore @cacheable @defines_strategy(eager=True) def deferred(definition: Callable[[], SearchStrategy[Ex]]) -> SearchStrategy[Ex]: """A deferred strategy allows you to write a strategy that references other strategies that have not yet been defined. This allows for the easy definition of recursive and mutually recursive strategies. The definition argument should be a zero-argument function that returns a strategy. It will be evaluated the first time the strategy is used to produce an example. Example usage: >>> import hypothesis.strategies as st >>> x = st.deferred(lambda: st.booleans() | st.tuples(x, x)) >>> x.example() (((False, (True, True)), (False, True)), (True, True)) >>> x.example() True Mutual recursion also works fine: >>> a = st.deferred(lambda: st.booleans() | b) >>> b = st.deferred(lambda: st.tuples(a, a)) >>> a.example() True >>> b.example() (False, (False, ((False, True), False))) Examples from this strategy shrink as they normally would from the strategy returned by the definition. """ return DeferredStrategy(definition) def domains() -> SearchStrategy[str]: import hypothesis.provisional return hypothesis.provisional.domains() @defines_strategy(force_reusable_values=True) def emails( *, domains: SearchStrategy[str] = LazyStrategy(domains, (), {}) ) -> SearchStrategy[str]: """A strategy for generating email addresses as unicode strings. The address format is specified in :rfc:`5322#section-3.4.1`. Values shrink towards shorter local-parts and host domains. If ``domains`` is given then it must be a strategy that generates domain names for the emails, defaulting to :func:`~hypothesis.provisional.domains`. This strategy is useful for generating "user data" for tests, as mishandling of email addresses is a common source of bugs. """ local_chars = string.ascii_letters + string.digits + "!#$%&'*+-/=^_`{|}~" local_part = text(local_chars, min_size=1, max_size=64) # TODO: include dot-atoms, quoted strings, escaped chars, etc in local part return builds("{}@{}".format, local_part, domains).filter( lambda addr: len(addr) <= 254 ) def _functions(*, like, returns, pure): # Wrapped up to use ParamSpec below check_type(bool, pure, "pure") if not callable(like): raise InvalidArgument( "The first argument to functions() must be a callable to imitate, " f"but got non-callable like={nicerepr(like)!r}" ) if returns in (None, ...): # Passing `None` has never been *documented* as working, but it still # did from May 2020 to Jan 2022 so we'll avoid breaking it without cause. hints = get_type_hints(like) returns = from_type(hints.get("return", type(None))) check_strategy(returns, "returns") return FunctionStrategy(like, returns, pure) if typing.TYPE_CHECKING or ParamSpec is not None: @overload def functions( *, pure: bool = ... ) -> SearchStrategy[Callable[[], None]]: # pragma: no cover ... @overload def functions( *, like: Callable[P, T], pure: bool = ..., ) -> SearchStrategy[Callable[P, T]]: # pragma: no cover ... @overload def functions( *, returns: SearchStrategy[T], pure: bool = ..., ) -> SearchStrategy[Callable[[], T]]: # pragma: no cover ... @overload def functions( *, like: Callable[P, Any], returns: SearchStrategy[T], pure: bool = ..., ) -> SearchStrategy[Callable[P, T]]: # pragma: no cover ... @defines_strategy() def functions(*, like=lambda: None, returns=..., pure=False): # We shouldn't need overloads here, but mypy disallows default args for # generics: https://github.com/python/mypy/issues/3737 """functions(*, like=lambda: None, returns=..., pure=False) A strategy for functions, which can be used in callbacks. The generated functions will mimic the interface of ``like``, which must be a callable (including a class, method, or function). The return value for the function is drawn from the ``returns`` argument, which must be a strategy. If ``returns`` is not passed, we attempt to infer a strategy from the return-type annotation if present, falling back to :func:`~none`. If ``pure=True``, all arguments passed to the generated function must be hashable, and if passed identical arguments the original return value will be returned again - *not* regenerated, so beware mutable values. If ``pure=False``, generated functions do not validate their arguments, and may return a different value if called again with the same arguments. Generated functions can only be called within the scope of the ``@given`` which created them. """ return _functions(like=like, returns=returns, pure=pure) else: # pragma: no cover @defines_strategy() def functions( *, like: Callable[..., Any] = lambda: None, returns: SearchStrategy[Any] | EllipsisType = ..., pure: bool = False, ) -> SearchStrategy[Callable[..., Any]]: """functions(*, like=lambda: None, returns=..., pure=False) A strategy for functions, which can be used in callbacks. The generated functions will mimic the interface of ``like``, which must be a callable (including a class, method, or function). The return value for the function is drawn from the ``returns`` argument, which must be a strategy. If ``returns`` is not passed, we attempt to infer a strategy from the return-type annotation if present, falling back to :func:`~none`. If ``pure=True``, all arguments passed to the generated function must be hashable, and if passed identical arguments the original return value will be returned again - *not* regenerated, so beware mutable values. If ``pure=False``, generated functions do not validate their arguments, and may return a different value if called again with the same arguments. Generated functions can only be called within the scope of the ``@given`` which created them. """ return _functions(like=like, returns=returns, pure=pure) @composite def slices(draw: Any, size: int) -> slice: """Generates slices that will select indices up to the supplied size Generated slices will have start and stop indices that range from -size to size - 1 and will step in the appropriate direction. Slices should only produce an empty selection if the start and end are the same. Examples from this strategy shrink toward 0 and smaller values """ check_valid_size(size, "size") if size == 0: step = draw(none() | integers().filter(bool)) return slice(None, None, step) # For slices start is inclusive and stop is exclusive start = draw(integers(0, size - 1) | none()) stop = draw(integers(0, size) | none()) # Limit step size to be reasonable if start is None and stop is None: max_step = size elif start is None: max_step = stop elif stop is None: max_step = start else: max_step = abs(start - stop) step = draw(integers(1, max_step or 1)) if (draw(booleans()) and start == stop) or (stop or 0) < (start or 0): step *= -1 if draw(booleans()) and start is not None: start -= size if draw(booleans()) and stop is not None: stop -= size if (not draw(booleans())) and step == 1: step = None return slice(start, stop, step) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/datetime.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime as dt import operator as op import warnings import zoneinfo from calendar import monthrange from functools import cache, partial from importlib import resources from pathlib import Path from hypothesis.errors import InvalidArgument from hypothesis.internal.validation import check_type, check_valid_interval from hypothesis.strategies._internal.core import sampled_from from hypothesis.strategies._internal.misc import just, none, nothing from hypothesis.strategies._internal.strategies import SearchStrategy from hypothesis.strategies._internal.utils import defines_strategy DATENAMES = ("year", "month", "day") TIMENAMES = ("hour", "minute", "second", "microsecond") def is_pytz_timezone(tz): if not isinstance(tz, dt.tzinfo): return False module = type(tz).__module__ return module == "pytz" or module.startswith("pytz.") def replace_tzinfo(value, timezone): if is_pytz_timezone(timezone): # Pytz timezones are a little complicated, and using the .replace method # can cause some weird issues, so we use their special "localize" instead. # # We use the fold attribute as a convenient boolean for is_dst, even though # they're semantically distinct. For ambiguous or imaginary hours, fold says # whether you should use the offset that applies before the gap (fold=0) or # the offset that applies after the gap (fold=1). is_dst says whether you # should choose the side that is "DST" or "STD" (STD->STD or DST->DST # transitions are unclear as you might expect). # # WARNING: this is INCORRECT for timezones with negative DST offsets such as # "Europe/Dublin", but it's unclear what we could do instead beyond # documenting the problem and recommending use of `dateutil` instead. return timezone.localize(value, is_dst=not value.fold) return value.replace(tzinfo=timezone) def datetime_does_not_exist(value): """This function tests whether the given datetime can be round-tripped to and from UTC. It is an exact inverse of (and very similar to) the dateutil method https://dateutil.readthedocs.io/en/stable/tz.html#dateutil.tz.datetime_exists """ # Naive datetimes cannot be imaginary, but we need this special case because # chaining .astimezone() ends with *the system local timezone*, not None. # See bug report in https://github.com/HypothesisWorks/hypothesis/issues/2662 if value.tzinfo is None: return False try: # Does the naive portion of the datetime change when round-tripped to # UTC? If so, or if this overflows, we say that it does not exist. roundtrip = value.astimezone(dt.timezone.utc).astimezone(value.tzinfo) except OverflowError: # Overflows at datetime.min or datetime.max boundary condition. # Rejecting these is acceptable, because timezones are close to # meaningless before ~1900 and subject to a lot of change by # 9999, so it should be a very small fraction of possible values. return True if ( value.tzinfo is not roundtrip.tzinfo and value.utcoffset() != roundtrip.utcoffset() ): # This only ever occurs during imaginary (i.e. nonexistent) datetimes, # and only for pytz timezones which do not follow PEP-495 semantics. # (may exclude a few other edge cases, but you should use zoneinfo anyway) return True assert value.tzinfo is roundtrip.tzinfo, "so only the naive portions are compared" return value != roundtrip def draw_capped_multipart( data, min_value, max_value, duration_names=DATENAMES + TIMENAMES ): assert isinstance(min_value, (dt.date, dt.time, dt.datetime)) assert type(min_value) == type(max_value) assert min_value <= max_value result = {} cap_low, cap_high = True, True for name in duration_names: low = getattr(min_value if cap_low else dt.datetime.min, name) high = getattr(max_value if cap_high else dt.datetime.max, name) if name == "day" and not cap_high: _, high = monthrange(**result) if name == "year": val = data.draw_integer(low, high, shrink_towards=2000) else: val = data.draw_integer(low, high) result[name] = val cap_low = cap_low and val == low cap_high = cap_high and val == high if hasattr(min_value, "fold"): # The `fold` attribute is ignored in comparison of naive datetimes. # In tz-aware datetimes it would require *very* invasive changes to # the logic above, and be very sensitive to the specific timezone # (at the cost of efficient shrinking and mutation), so at least for # now we stick with the status quo and generate it independently. result["fold"] = data.draw_integer(0, 1) return result class DatetimeStrategy(SearchStrategy): def __init__(self, min_value, max_value, timezones_strat, allow_imaginary): super().__init__() assert isinstance(min_value, dt.datetime) assert isinstance(max_value, dt.datetime) assert min_value.tzinfo is None assert max_value.tzinfo is None assert min_value <= max_value assert isinstance(timezones_strat, SearchStrategy) assert isinstance(allow_imaginary, bool) self.min_value = min_value self.max_value = max_value self.tz_strat = timezones_strat self.allow_imaginary = allow_imaginary def do_draw(self, data): # We start by drawing a timezone, and an initial datetime. tz = data.draw(self.tz_strat) result = self.draw_naive_datetime_and_combine(data, tz) # TODO: with some probability, systematically search for one of # - an imaginary time (if allowed), # - a time within 24hrs of a leap second (if there any are within bounds), # - other subtle, little-known, or nasty issues as described in # https://github.com/HypothesisWorks/hypothesis/issues/69 # If we happened to end up with a disallowed imaginary time, reject it. if (not self.allow_imaginary) and datetime_does_not_exist(result): data.mark_invalid(f"{result} does not exist (usually a DST transition)") return result def draw_naive_datetime_and_combine(self, data, tz): result = draw_capped_multipart(data, self.min_value, self.max_value) try: return replace_tzinfo(dt.datetime(**result), timezone=tz) except (ValueError, OverflowError): data.mark_invalid( f"Failed to draw a datetime between {self.min_value!r} and " f"{self.max_value!r} with timezone from {self.tz_strat!r}." ) @defines_strategy(force_reusable_values=True) def datetimes( min_value: dt.datetime = dt.datetime.min, max_value: dt.datetime = dt.datetime.max, *, timezones: SearchStrategy[dt.tzinfo | None] = none(), allow_imaginary: bool = True, ) -> SearchStrategy[dt.datetime]: """datetimes(min_value=datetime.datetime.min, max_value=datetime.datetime.max, *, timezones=none(), allow_imaginary=True) A strategy for generating datetimes, which may be timezone-aware. This strategy works by drawing a naive datetime between ``min_value`` and ``max_value``, which must both be naive (have no timezone). ``timezones`` must be a strategy that generates either ``None``, for naive datetimes, or :class:`~python:datetime.tzinfo` objects for 'aware' datetimes. You can construct your own, though we recommend using one of these built-in strategies: * with the standard library: :func:`hypothesis.strategies.timezones`; * with :pypi:`dateutil `: :func:`hypothesis.extra.dateutil.timezones`; or * with :pypi:`pytz`: :func:`hypothesis.extra.pytz.timezones`. You may pass ``allow_imaginary=False`` to filter out "imaginary" datetimes which did not (or will not) occur due to daylight savings, leap seconds, timezone and calendar adjustments, etc. Imaginary datetimes are allowed by default, because malformed timestamps are a common source of bugs. Examples from this strategy shrink towards midnight on January 1st 2000, local time. """ # Why must bounds be naive? In principle, we could also write a strategy # that took aware bounds, but the API and validation is much harder. # If you want to generate datetimes between two particular moments in # time I suggest (a) just filtering out-of-bounds values; (b) if bounds # are very close, draw a value and subtract its UTC offset, handling # overflows and nonexistent times; or (c) do something customised to # handle datetimes in e.g. a four-microsecond span which is not # representable in UTC. Handling (d), all of the above, leads to a much # more complex API for all users and a useful feature for very few. check_type(bool, allow_imaginary, "allow_imaginary") check_type(dt.datetime, min_value, "min_value") check_type(dt.datetime, max_value, "max_value") if min_value.tzinfo is not None: raise InvalidArgument(f"{min_value=} must not have tzinfo") if max_value.tzinfo is not None: raise InvalidArgument(f"{max_value=} must not have tzinfo") check_valid_interval(min_value, max_value, "min_value", "max_value") if not isinstance(timezones, SearchStrategy): raise InvalidArgument( f"{timezones=} must be a SearchStrategy that can " "provide tzinfo for datetimes (either None or dt.tzinfo objects)" ) return DatetimeStrategy(min_value, max_value, timezones, allow_imaginary) class TimeStrategy(SearchStrategy): def __init__(self, min_value, max_value, timezones_strat): super().__init__() self.min_value = min_value self.max_value = max_value self.tz_strat = timezones_strat def do_draw(self, data): result = draw_capped_multipart(data, self.min_value, self.max_value, TIMENAMES) tz = data.draw(self.tz_strat) return dt.time(**result, tzinfo=tz) @defines_strategy(force_reusable_values=True) def times( min_value: dt.time = dt.time.min, max_value: dt.time = dt.time.max, *, timezones: SearchStrategy[dt.tzinfo | None] = none(), ) -> SearchStrategy[dt.time]: """times(min_value=datetime.time.min, max_value=datetime.time.max, *, timezones=none()) A strategy for times between ``min_value`` and ``max_value``. The ``timezones`` argument is handled as for :py:func:`datetimes`. Examples from this strategy shrink towards midnight, with the timezone component shrinking as for the strategy that provided it. """ check_type(dt.time, min_value, "min_value") check_type(dt.time, max_value, "max_value") if min_value.tzinfo is not None: raise InvalidArgument(f"{min_value=} must not have tzinfo") if max_value.tzinfo is not None: raise InvalidArgument(f"{max_value=} must not have tzinfo") check_valid_interval(min_value, max_value, "min_value", "max_value") return TimeStrategy(min_value, max_value, timezones) class DateStrategy(SearchStrategy): def __init__(self, min_value, max_value): super().__init__() assert isinstance(min_value, dt.date) assert isinstance(max_value, dt.date) assert min_value < max_value self.min_value = min_value self.max_value = max_value def do_draw(self, data): return dt.date( **draw_capped_multipart(data, self.min_value, self.max_value, DATENAMES) ) def filter(self, condition): if ( isinstance(condition, partial) and len(args := condition.args) == 1 and not condition.keywords and isinstance(arg := args[0], dt.date) and condition.func in (op.lt, op.le, op.eq, op.ge, op.gt) ): try: arg += dt.timedelta(days={op.lt: 1, op.gt: -1}.get(condition.func, 0)) except OverflowError: # gt date.max, or lt date.min return nothing() lo, hi = { # We're talking about op(arg, x) - the reverse of our usual intuition! op.lt: (arg, self.max_value), # lambda x: arg < x op.le: (arg, self.max_value), # lambda x: arg <= x op.eq: (arg, arg), # lambda x: arg == x op.ge: (self.min_value, arg), # lambda x: arg >= x op.gt: (self.min_value, arg), # lambda x: arg > x }[condition.func] lo = max(lo, self.min_value) hi = min(hi, self.max_value) print(lo, hi) if hi < lo: return nothing() if lo <= self.min_value and self.max_value <= hi: return self return dates(lo, hi) return super().filter(condition) @defines_strategy(force_reusable_values=True) def dates( min_value: dt.date = dt.date.min, max_value: dt.date = dt.date.max ) -> SearchStrategy[dt.date]: """dates(min_value=datetime.date.min, max_value=datetime.date.max) A strategy for dates between ``min_value`` and ``max_value``. Examples from this strategy shrink towards January 1st 2000. """ check_type(dt.date, min_value, "min_value") check_type(dt.date, max_value, "max_value") check_valid_interval(min_value, max_value, "min_value", "max_value") if min_value == max_value: return just(min_value) return DateStrategy(min_value, max_value) class TimedeltaStrategy(SearchStrategy): def __init__(self, min_value, max_value): super().__init__() assert isinstance(min_value, dt.timedelta) assert isinstance(max_value, dt.timedelta) assert min_value < max_value self.min_value = min_value self.max_value = max_value def do_draw(self, data): result = {} low_bound = True high_bound = True for name in ("days", "seconds", "microseconds"): low = getattr(self.min_value if low_bound else dt.timedelta.min, name) high = getattr(self.max_value if high_bound else dt.timedelta.max, name) val = data.draw_integer(low, high) result[name] = val low_bound = low_bound and val == low high_bound = high_bound and val == high return dt.timedelta(**result) @defines_strategy(force_reusable_values=True) def timedeltas( min_value: dt.timedelta = dt.timedelta.min, max_value: dt.timedelta = dt.timedelta.max, ) -> SearchStrategy[dt.timedelta]: """timedeltas(min_value=datetime.timedelta.min, max_value=datetime.timedelta.max) A strategy for timedeltas between ``min_value`` and ``max_value``. Examples from this strategy shrink towards zero. """ check_type(dt.timedelta, min_value, "min_value") check_type(dt.timedelta, max_value, "max_value") check_valid_interval(min_value, max_value, "min_value", "max_value") if min_value == max_value: return just(min_value) return TimedeltaStrategy(min_value=min_value, max_value=max_value) @cache def _valid_key_cacheable(tzpath, key): assert isinstance(tzpath, tuple) # zoneinfo changed, better update this function! for root in tzpath: if Path(root).joinpath(key).exists(): # pragma: no branch # No branch because most systems only have one TZPATH component. return True else: # pragma: no cover # This branch is only taken for names which are known to zoneinfo # but not present on the filesystem, i.e. on Windows with tzdata, # and so is never executed by our coverage tests. *package_loc, resource_name = key.split("/") package = "tzdata.zoneinfo." + ".".join(package_loc) try: return (resources.files(package) / resource_name).exists() except ModuleNotFoundError: return False @defines_strategy(force_reusable_values=True) def timezone_keys( *, # allow_alias: bool = True, # allow_deprecated: bool = True, allow_prefix: bool = True, ) -> SearchStrategy[str]: """A strategy for :wikipedia:`IANA timezone names `. As well as timezone names like ``"UTC"``, ``"Australia/Sydney"``, or ``"America/New_York"``, this strategy can generate: - Aliases such as ``"Antarctica/McMurdo"``, which links to ``"Pacific/Auckland"``. - Deprecated names such as ``"Antarctica/South_Pole"``, which *also* links to ``"Pacific/Auckland"``. Note that most but not all deprecated timezone names are also aliases. - Timezone names with the ``"posix/"`` or ``"right/"`` prefixes, unless ``allow_prefix=False``. These strings are provided separately from Tzinfo objects - such as ZoneInfo instances from the timezones() strategy - to facilitate testing of timezone logic without needing workarounds to access non-canonical names. .. note:: `The tzdata package is required on Windows `__. ``pip install hypothesis[zoneinfo]`` installs it, if and only if needed. On Windows, you may need to access IANA timezone data via the :pypi:`tzdata` package. For non-IANA timezones, such as Windows-native names or GNU TZ strings, we recommend using :func:`~hypothesis.strategies.sampled_from` with the :pypi:`dateutil ` package, e.g. :meth:`dateutil:dateutil.tz.tzwin.list`. """ # check_type(bool, allow_alias, "allow_alias") # check_type(bool, allow_deprecated, "allow_deprecated") check_type(bool, allow_prefix, "allow_prefix") with warnings.catch_warnings(): try: warnings.simplefilter("ignore", EncodingWarning) except NameError: # pragma: no cover pass # On Python 3.12 (and others?), `available_timezones()` opens files # without specifying an encoding - which our selftests make an error. available_timezones = ("UTC", *sorted(zoneinfo.available_timezones())) # TODO: filter out alias and deprecated names if disallowed # When prefixes are allowed, we first choose a key and then flatmap to get our # choice with one of the available prefixes. That in turn means that we need # some logic to determine which prefixes are available for a given key: def valid_key(key): return key == "UTC" or _valid_key_cacheable(zoneinfo.TZPATH, key) # TODO: work out how to place a higher priority on "weird" timezones # For details see https://github.com/HypothesisWorks/hypothesis/issues/2414 strategy = sampled_from([key for key in available_timezones if valid_key(key)]) if not allow_prefix: return strategy def sample_with_prefixes(zone): keys_with_prefixes = (zone, f"posix/{zone}", f"right/{zone}") return sampled_from([key for key in keys_with_prefixes if valid_key(key)]) return strategy.flatmap(sample_with_prefixes) @defines_strategy(force_reusable_values=True) def timezones(*, no_cache: bool = False) -> SearchStrategy["zoneinfo.ZoneInfo"]: """A strategy for :class:`python:zoneinfo.ZoneInfo` objects. If ``no_cache=True``, the generated instances are constructed using :meth:`ZoneInfo.no_cache ` instead of the usual constructor. This may change the semantics of your datetimes in surprising ways, so only use it if you know that you need to! .. note:: `The tzdata package is required on Windows `__. ``pip install hypothesis[zoneinfo]`` installs it, if and only if needed. """ check_type(bool, no_cache, "no_cache") return timezone_keys().map( zoneinfo.ZoneInfo.no_cache if no_cache else zoneinfo.ZoneInfo ) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/deferred.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import inspect from collections.abc import Callable, Sequence from hypothesis.configuration import check_sideeffect_during_initialization from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.strategies import ( Ex, RecurT, SearchStrategy, check_strategy, ) class DeferredStrategy(SearchStrategy[Ex]): """A strategy which may be used before it is fully defined.""" def __init__(self, definition: Callable[[], SearchStrategy[Ex]]): super().__init__() self.__wrapped_strategy: SearchStrategy[Ex] | None = None self.__in_repr: bool = False self.__definition: Callable[[], SearchStrategy[Ex]] | None = definition @property def wrapped_strategy(self) -> SearchStrategy[Ex]: # we assign this before entering the condition to avoid a race condition # under threading. See issue #4523. definition = self.__definition if self.__wrapped_strategy is None: check_sideeffect_during_initialization("deferred evaluation of {!r}", self) if not inspect.isfunction(definition): raise InvalidArgument( f"Expected definition to be a function but got {definition!r} " f"of type {type(definition).__name__} instead." ) result = definition() if result is self: raise InvalidArgument("Cannot define a deferred strategy to be itself") check_strategy(result, "definition()") self.__wrapped_strategy = result self.__definition = None return self.__wrapped_strategy @property def branches(self) -> Sequence[SearchStrategy[Ex]]: return self.wrapped_strategy.branches def calc_label(self) -> int: """Deferred strategies don't have a calculated label, because we would end up having to calculate the fixed point of some hash function in order to calculate it when they recursively refer to themself! The label for the wrapped strategy will still appear because it will be passed to draw. """ # This is actually the same as the parent class implementation, but we # include it explicitly here in order to document that this is a # deliberate decision. return self.class_label def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.wrapped_strategy) def calc_has_reusable_values(self, recur: RecurT) -> bool: return recur(self.wrapped_strategy) def __repr__(self) -> str: if self.__wrapped_strategy is not None: if self.__in_repr: return f"(deferred@{id(self)!r})" try: self.__in_repr = True return repr(self.__wrapped_strategy) finally: self.__in_repr = False else: description = get_pretty_function_description(self.__definition) return f"deferred({description})" def do_draw(self, data: ConjectureData) -> Ex: return data.draw(self.wrapped_strategy) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/featureflags.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Hashable, Iterable, Sequence from typing import Any from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.strategies._internal.strategies import SearchStrategy FEATURE_LABEL = cu.calc_label_from_name("feature flag") class FeatureFlags: """Object that can be used to control a number of feature flags for a given test run. This enables an approach to data generation called swarm testing ( see Groce, Alex, et al. "Swarm testing." Proceedings of the 2012 International Symposium on Software Testing and Analysis. ACM, 2012), in which generation is biased by selectively turning some features off for each test case generated. When there are many interacting features this can find bugs that a pure generation strategy would otherwise have missed. FeatureFlags are designed to "shrink open", so that during shrinking they become less restrictive. This allows us to potentially shrink to smaller test cases that were forbidden during the generation phase because they required disabled features. """ def __init__( self, data: ConjectureData | None = None, enabled: Sequence[Any] = (), disabled: Sequence[Any] = (), at_least_one_of: Iterable[Hashable] = (), ): self.__data = data self.__is_disabled = {} for f in enabled: self.__is_disabled[f] = False for f in disabled: self.__is_disabled[f] = True # In the original swarm testing paper they turn features on or off # uniformly at random. Instead we decide the probability with which to # enable features up front. This can allow for scenarios where all or # no features are enabled, which are vanishingly unlikely in the # original model. # # We implement this as a single 8-bit integer and enable features which # score >= that value. In particular when self.__baseline is 0, all # features will be enabled. This is so that we shrink in the direction # of more features being enabled. if self.__data is not None: self.__p_disabled = self.__data.draw_integer(0, 254) / 255 else: # If data is None we're in example mode so all that matters is the # enabled/disabled lists above. We set this up so that everything # else is enabled by default. self.__p_disabled = 0.0 # The naive approach can lead to disabling e.g. every single rule on a # RuleBasedStateMachine, which aborts the test as unable to make progress. # Track the set of possible names, and ensure that at least one is enabled. self.__at_least_one_of = set(at_least_one_of) def is_enabled(self, name: Any) -> bool: """Tests whether the feature named ``name`` should be enabled on this test run.""" if self.__data is None or self.__data.frozen: # Feature set objects might hang around after data generation has # finished. If this happens then we just report all new features as # enabled, because that's our shrinking direction and they have no # impact on data generation if they weren't used while it was # running. return not self.__is_disabled.get(name, False) data = self.__data data.start_span(label=FEATURE_LABEL) # If we've already decided on this feature then we don't actually # need to draw anything, but we do write the same decision to the # input stream. This allows us to lazily decide whether a feature # is enabled, because it means that if we happen to delete the part # of the test case where we originally decided, the next point at # which we make this decision just makes the decision it previously # made. oneof = self.__at_least_one_of is_disabled = self.__data.draw_boolean( self.__p_disabled, forced=( False if len(oneof) == 1 and name in oneof else self.__is_disabled.get(name) ), ) self.__is_disabled[name] = is_disabled if name in oneof and not is_disabled: oneof.clear() oneof.discard(name) data.stop_span() return not is_disabled def __repr__(self) -> str: enabled = [] disabled = [] for name, is_disabled in self.__is_disabled.items(): if is_disabled: disabled.append(name) else: enabled.append(name) return f"FeatureFlags({enabled=}, {disabled=})" class FeatureStrategy(SearchStrategy[FeatureFlags]): def __init__(self, at_least_one_of: Iterable[Hashable] = ()): super().__init__() self._at_least_one_of = frozenset(at_least_one_of) def do_draw(self, data: ConjectureData) -> FeatureFlags: return FeatureFlags(data, at_least_one_of=self._at_least_one_of) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/flatmapped.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Callable from typing import Generic, TypeVar from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.utils import ( calc_label_from_callable, combine_labels, ) from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.strategies import ( RecurT, SearchStrategy, check_strategy, ) MappedFrom = TypeVar("MappedFrom") MappedTo = TypeVar("MappedTo") class FlatMapStrategy(SearchStrategy[MappedTo], Generic[MappedFrom, MappedTo]): def __init__( self, base: SearchStrategy[MappedFrom], expand: Callable[[MappedFrom], SearchStrategy[MappedTo]], ): super().__init__() self.base = base self.expand = expand def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.base) def calc_label(self) -> int: return combine_labels( self.class_label, self.base.label, calc_label_from_callable(self.expand), ) def __repr__(self) -> str: if not hasattr(self, "_cached_repr"): self._cached_repr = ( f"{self.base!r}.flatmap({get_pretty_function_description(self.expand)})" ) return self._cached_repr def do_draw(self, data: ConjectureData) -> MappedTo: base = data.draw(self.base) expanded = self.expand(base) check_strategy(expanded) return data.draw(expanded) @property def branches(self) -> list[SearchStrategy[MappedTo]]: return [ FlatMapStrategy(strategy, expand=self.expand) for strategy in self.base.branches ] ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/functions.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from weakref import WeakKeyDictionary from hypothesis.control import note, should_note from hypothesis.errors import InvalidState from hypothesis.internal.reflection import ( convert_positional_arguments, nicerepr, proxies, repr_call, ) from hypothesis.strategies._internal.strategies import RecurT, SearchStrategy class FunctionStrategy(SearchStrategy): def __init__(self, like, returns, pure): super().__init__() self.like = like self.returns = returns self.pure = pure # Using wekrefs-to-generated-functions means that the cache can be # garbage-collected at the end of each example, reducing memory use. self._cache = WeakKeyDictionary() def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.returns) def do_draw(self, data): @proxies(self.like) def inner(*args, **kwargs): if data.frozen: raise InvalidState( f"This generated {nicerepr(self.like)} function can only " "be called within the scope of the @given that created it." ) if self.pure: args, kwargs = convert_positional_arguments(self.like, args, kwargs) key = (args, frozenset(kwargs.items())) cache = self._cache.setdefault(inner, {}) if key not in cache: cache[key] = data.draw(self.returns) if should_note(): # optimization to avoid needless repr_call rep = repr_call(self.like, args, kwargs, reorder=False) note(f"Called function: {rep} -> {cache[key]!r}") return cache[key] else: val = data.draw(self.returns) if should_note(): rep = repr_call(self.like, args, kwargs, reorder=False) note(f"Called function: {rep} -> {val!r}") return val return inner ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/ipaddress.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network from typing import Literal from hypothesis.errors import InvalidArgument from hypothesis.internal.validation import check_type from hypothesis.strategies._internal.core import binary, sampled_from from hypothesis.strategies._internal.numbers import integers from hypothesis.strategies._internal.strategies import SearchStrategy from hypothesis.strategies._internal.utils import defines_strategy # See https://www.iana.org/assignments/iana-ipv4-special-registry/ SPECIAL_IPv4_RANGES = ( "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.0.0/29", "192.0.0.8/32", "192.0.0.9/32", "192.0.0.10/32", "192.0.0.170/32", "192.0.0.171/32", "192.0.2.0/24", "192.31.196.0/24", "192.52.193.0/24", "192.88.99.0/24", "192.168.0.0/16", "192.175.48.0/24", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "240.0.0.0/4", "255.255.255.255/32", ) # and https://www.iana.org/assignments/iana-ipv6-special-registry/ SPECIAL_IPv6_RANGES = ( "::1/128", "::/128", "::ffff:0:0/96", "64:ff9b::/96", "64:ff9b:1::/48", "100::/64", "2001::/23", "2001::/32", "2001:1::1/128", "2001:1::2/128", "2001:2::/48", "2001:3::/32", "2001:4:112::/48", "2001:10::/28", "2001:20::/28", "2001:db8::/32", "2002::/16", "2620:4f:8000::/48", "fc00::/7", "fe80::/10", ) @defines_strategy(force_reusable_values=True) def ip_addresses( *, v: Literal[4, 6] | None = None, network: str | IPv4Network | IPv6Network | None = None, ) -> SearchStrategy[IPv4Address | IPv6Address]: r"""Generate IP addresses - ``v=4`` for :class:`~python:ipaddress.IPv4Address`\ es, ``v=6`` for :class:`~python:ipaddress.IPv6Address`\ es, or leave unspecified to allow both versions. ``network`` may be an :class:`~python:ipaddress.IPv4Network` or :class:`~python:ipaddress.IPv6Network`, or a string representing a network such as ``"127.0.0.0/24"`` or ``"2001:db8::/32"``. As well as generating addresses within a particular routable network, this can be used to generate addresses from a reserved range listed in the `IANA `__ `registries `__. If you pass both ``v`` and ``network``, they must be for the same version. """ if v is not None: check_type(int, v, "v") if v not in (4, 6): raise InvalidArgument(f"{v=}, but only v=4 or v=6 are valid") if network is None: # We use the reserved-address registries to boost the chance # of generating one of the various special types of address. four = binary(min_size=4, max_size=4).map(IPv4Address) | sampled_from( SPECIAL_IPv4_RANGES ).flatmap(lambda network: ip_addresses(network=network)) six = binary(min_size=16, max_size=16).map(IPv6Address) | sampled_from( SPECIAL_IPv6_RANGES ).flatmap(lambda network: ip_addresses(network=network)) if v == 4: return four if v == 6: return six return four | six if isinstance(network, str): network = ip_network(network) check_type((IPv4Network, IPv6Network), network, "network") assert isinstance(network, (IPv4Network, IPv6Network)) # for Mypy if v not in (None, network.version): raise InvalidArgument(f"{v=} is incompatible with {network=}") addr_type = IPv4Address if network.version == 4 else IPv6Address return integers(int(network[0]), int(network[-1])).map(addr_type) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/lazy.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Callable, Sequence from inspect import signature from typing import Any from weakref import WeakKeyDictionary from hypothesis.configuration import check_sideeffect_during_initialization from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.reflection import ( convert_keyword_arguments, convert_positional_arguments, get_pretty_function_description, repr_call, ) from hypothesis.strategies._internal.deferred import DeferredStrategy from hypothesis.strategies._internal.strategies import Ex, RecurT, SearchStrategy from hypothesis.utils.threading import ThreadLocal threadlocal = ThreadLocal(unwrap_depth=int, unwrap_cache=WeakKeyDictionary) def unwrap_strategies(s): # optimization if not isinstance(s, (LazyStrategy, DeferredStrategy)): return s try: return threadlocal.unwrap_cache[s] except KeyError: pass threadlocal.unwrap_cache[s] = s threadlocal.unwrap_depth += 1 try: result = unwrap_strategies(s.wrapped_strategy) threadlocal.unwrap_cache[s] = result try: assert result.force_has_reusable_values == s.force_has_reusable_values except AttributeError: pass try: result.force_has_reusable_values = s.force_has_reusable_values except AttributeError: pass return result finally: threadlocal.unwrap_depth -= 1 if threadlocal.unwrap_depth <= 0: threadlocal.unwrap_cache.clear() assert threadlocal.unwrap_depth >= 0 class LazyStrategy(SearchStrategy[Ex]): """A strategy which is defined purely by conversion to and from another strategy. Its parameter and distribution come from that other strategy. """ def __init__( self, function: Callable[..., SearchStrategy[Ex]], args: Sequence[object], kwargs: dict[str, object], *, transforms: tuple[tuple[str, Callable[..., Any]], ...] = (), force_repr: str | None = None, ): super().__init__() self.__wrapped_strategy: SearchStrategy[Ex] | None = None self.__representation: str | None = force_repr self.function = function self.__args = args self.__kwargs = kwargs self._transformations = transforms def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.wrapped_strategy) def calc_has_reusable_values(self, recur: RecurT) -> bool: return recur(self.wrapped_strategy) def calc_is_cacheable(self, recur: RecurT) -> bool: for source in (self.__args, self.__kwargs.values()): for v in source: if isinstance(v, SearchStrategy) and not v.is_cacheable: return False return True def calc_label(self) -> int: return self.wrapped_strategy.label @property def wrapped_strategy(self) -> SearchStrategy[Ex]: if self.__wrapped_strategy is None: check_sideeffect_during_initialization("lazy evaluation of {!r}", self) unwrapped_args = tuple(unwrap_strategies(s) for s in self.__args) unwrapped_kwargs = { k: unwrap_strategies(v) for k, v in self.__kwargs.items() } base = self.function(*self.__args, **self.__kwargs) if unwrapped_args == self.__args and unwrapped_kwargs == self.__kwargs: _wrapped_strategy = base else: _wrapped_strategy = self.function(*unwrapped_args, **unwrapped_kwargs) for method, fn in self._transformations: _wrapped_strategy = getattr(_wrapped_strategy, method)(fn) self.__wrapped_strategy = _wrapped_strategy assert self.__wrapped_strategy is not None return self.__wrapped_strategy def __with_transform(self, method, fn): repr_ = self.__representation if repr_: repr_ = f"{repr_}.{method}({get_pretty_function_description(fn)})" return LazyStrategy( self.function, self.__args, self.__kwargs, transforms=(*self._transformations, (method, fn)), force_repr=repr_, ) def map(self, pack): return self.__with_transform("map", pack) def filter(self, condition): return self.__with_transform("filter", condition) def do_validate(self) -> None: w = self.wrapped_strategy assert isinstance(w, SearchStrategy), f"{self!r} returned non-strategy {w!r}" w.validate() def __repr__(self) -> str: if self.__representation is None: sig = signature(self.function) pos = [p for p in sig.parameters.values() if "POSITIONAL" in p.kind.name] if len(pos) > 1 or any(p.default is not sig.empty for p in pos): _args, _kwargs = convert_positional_arguments( self.function, self.__args, self.__kwargs ) else: _args, _kwargs = convert_keyword_arguments( self.function, self.__args, self.__kwargs ) kwargs_for_repr = { k: v for k, v in _kwargs.items() if k not in sig.parameters or v is not sig.parameters[k].default } self.__representation = repr_call( self.function, _args, kwargs_for_repr, reorder=False ) + "".join( f".{method}({get_pretty_function_description(fn)})" for method, fn in self._transformations ) return self.__representation def do_draw(self, data: ConjectureData) -> Ex: return data.draw(self.wrapped_strategy) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/misc.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Callable from typing import TYPE_CHECKING, Any, NoReturn from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.strategies import ( Ex, RecurT, SampledFromStrategy, SearchStrategy, T, is_hashable, ) from hypothesis.strategies._internal.utils import cacheable, defines_strategy from hypothesis.utils.conventions import UniqueIdentifier if TYPE_CHECKING: from typing_extensions import Never class JustStrategy(SampledFromStrategy[Ex]): """A strategy which always returns a single fixed value. It's implemented as a length-one SampledFromStrategy so that all our special-case logic for filtering and sets applies also to just(x). The important difference from a SampledFromStrategy with only one element to choose is that JustStrategy *never* touches the underlying choice sequence, i.e. drawing neither reads from nor writes to `data`. This is a reasonably important optimisation (or semantic distinction!) for both JustStrategy and SampledFromStrategy. """ @property def value(self) -> Ex: return self.elements[0] def __repr__(self) -> str: suffix = "".join( f".{name}({get_pretty_function_description(f)})" for name, f in self._transformations ) if self.value is None: return "none()" + suffix return f"just({get_pretty_function_description(self.value)}){suffix}" def calc_is_cacheable(self, recur: RecurT) -> bool: return is_hashable(self.value) def do_filtered_draw(self, data: ConjectureData) -> Ex | UniqueIdentifier: # The parent class's `do_draw` implementation delegates directly to # `do_filtered_draw`, which we can greatly simplify in this case since # we have exactly one value. (This also avoids drawing any data.) return self._transform(self.value) @defines_strategy(eager=True) def just(value: T) -> SearchStrategy[T]: """Return a strategy which only generates ``value``. Note: ``value`` is not copied. Be wary of using mutable values. If ``value`` is the result of a callable, you can use :func:`builds(callable) ` instead of ``just(callable())`` to get a fresh value each time. Examples from this strategy do not shrink (because there is only one). """ return JustStrategy([value]) @defines_strategy(force_reusable_values=True) def none() -> SearchStrategy[None]: """Return a strategy which only generates None. Examples from this strategy do not shrink (because there is only one). """ return just(None) class Nothing(SearchStrategy["Never"]): def calc_is_empty(self, recur: RecurT) -> bool: return True def do_draw(self, data: ConjectureData) -> NoReturn: # This method should never be called because draw() will mark the # data as invalid immediately because is_empty is True. raise NotImplementedError("This should never happen") def calc_has_reusable_values(self, recur: RecurT) -> bool: return True def __repr__(self) -> str: return "nothing()" def map(self, pack: Callable[[Any], Any]) -> SearchStrategy["Never"]: return self def filter(self, condition: Callable[[Any], Any]) -> "SearchStrategy[Never]": return self def flatmap( self, expand: Callable[[Any], "SearchStrategy[Any]"] ) -> "SearchStrategy[Never]": return self NOTHING = Nothing() @cacheable @defines_strategy(eager=True) def nothing() -> SearchStrategy["Never"]: """This strategy never successfully draws a value and will always reject on an attempt to draw. Examples from this strategy do not shrink (because there are none). """ return NOTHING class BooleansStrategy(SearchStrategy[bool]): def do_draw(self, data: ConjectureData) -> bool: return data.draw_boolean() def __repr__(self) -> str: return "booleans()" ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/numbers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from decimal import Decimal from fractions import Fraction from typing import Literal, cast from hypothesis.control import reject from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.filtering import ( get_float_predicate_bounds, get_integer_predicate_bounds, ) from hypothesis.internal.floats import ( SMALLEST_SUBNORMAL, float_of, float_to_int, int_to_float, is_negative, next_down, next_down_normal, next_up, next_up_normal, width_smallest_normals, ) from hypothesis.internal.validation import ( check_type, check_valid_bound, check_valid_interval, ) from hypothesis.strategies._internal.misc import nothing from hypothesis.strategies._internal.strategies import ( SampledFromStrategy, SearchStrategy, ) from hypothesis.strategies._internal.utils import cacheable, defines_strategy # See https://github.com/python/mypy/issues/3186 - numbers.Real is wrong! Real = int | float | Fraction | Decimal class IntegersStrategy(SearchStrategy[int]): def __init__(self, start: int | None, end: int | None) -> None: super().__init__() assert isinstance(start, int) or start is None assert isinstance(end, int) or end is None assert start is None or end is None or start <= end self.start = start self.end = end def __repr__(self) -> str: if self.start is None and self.end is None: return "integers()" if self.end is None: return f"integers(min_value={self.start})" if self.start is None: return f"integers(max_value={self.end})" return f"integers({self.start}, {self.end})" def do_draw(self, data: ConjectureData) -> int: # For bounded integers, make the bounds and near-bounds more likely. weights = None if ( self.end is not None and self.start is not None and self.end - self.start > 127 ): weights = { self.start: (2 / 128), self.start + 1: (1 / 128), self.end - 1: (1 / 128), self.end: (2 / 128), } return data.draw_integer( min_value=self.start, max_value=self.end, weights=weights ) def filter(self, condition): if condition is math.isfinite: return self if condition in [math.isinf, math.isnan]: return nothing() constraints, pred = get_integer_predicate_bounds(condition) start, end = self.start, self.end if "min_value" in constraints: start = max(constraints["min_value"], -math.inf if start is None else start) if "max_value" in constraints: end = min(constraints["max_value"], math.inf if end is None else end) if start != self.start or end != self.end: if start is not None and end is not None and start > end: return nothing() self = type(self)(start, end) if pred is None: return self return super().filter(pred) @cacheable @defines_strategy(force_reusable_values=True) def integers( min_value: int | None = None, max_value: int | None = None, ) -> SearchStrategy[int]: """Returns a strategy which generates integers. If min_value is not None then all values will be >= min_value. If max_value is not None then all values will be <= max_value Examples from this strategy will shrink towards zero, and negative values will also shrink towards positive (i.e. -n may be replaced by +n). """ check_valid_bound(min_value, "min_value") check_valid_bound(max_value, "max_value") check_valid_interval(min_value, max_value, "min_value", "max_value") if min_value is not None: if min_value != int(min_value): raise InvalidArgument( f"min_value={min_value!r} of type {type(min_value)!r} " "cannot be exactly represented as an integer." ) min_value = int(min_value) if max_value is not None: if max_value != int(max_value): raise InvalidArgument( f"max_value={max_value!r} of type {type(max_value)!r} " "cannot be exactly represented as an integer." ) max_value = int(max_value) return IntegersStrategy(min_value, max_value) class FloatStrategy(SearchStrategy[float]): """A strategy for floating point numbers.""" def __init__( self, *, min_value: float, max_value: float, allow_nan: bool, # The smallest nonzero number we can represent is usually a subnormal, but may # be the smallest normal if we're running in unsafe denormals-are-zero mode. # While that's usually an explicit error, we do need to handle the case where # the user passes allow_subnormal=False. smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL, ): super().__init__() assert isinstance(allow_nan, bool) assert smallest_nonzero_magnitude >= 0.0, "programmer error if this is negative" if smallest_nonzero_magnitude == 0.0: # pragma: no cover raise FloatingPointError( "Got allow_subnormal=True, but we can't represent subnormal floats " "right now, in violation of the IEEE-754 floating-point " "specification. This is usually because something was compiled with " "-ffast-math or a similar option, which sets global processor state. " "See https://simonbyrne.github.io/notes/fastmath/ for a more detailed " "writeup - and good luck!" ) self.min_value = min_value self.max_value = max_value self.allow_nan = allow_nan self.smallest_nonzero_magnitude = smallest_nonzero_magnitude def __repr__(self) -> str: return ( f"{self.__class__.__name__}({self.min_value=}, {self.max_value=}, " f"{self.allow_nan=}, {self.smallest_nonzero_magnitude=})" ).replace("self.", "") def do_draw(self, data: ConjectureData) -> float: return data.draw_float( min_value=self.min_value, max_value=self.max_value, allow_nan=self.allow_nan, smallest_nonzero_magnitude=self.smallest_nonzero_magnitude, ) def filter(self, condition): # Handle a few specific weird cases. if condition is math.isfinite: return FloatStrategy( min_value=max(self.min_value, next_up(float("-inf"))), max_value=min(self.max_value, next_down(float("inf"))), allow_nan=False, smallest_nonzero_magnitude=self.smallest_nonzero_magnitude, ) if condition is math.isinf: if permitted_infs := [ x for x in (-math.inf, math.inf) if self.min_value <= x <= self.max_value ]: return SampledFromStrategy(permitted_infs) return nothing() if condition is math.isnan: if not self.allow_nan: return nothing() return NanStrategy() constraints, pred = get_float_predicate_bounds(condition) if not constraints: return super().filter(pred) min_bound = max(constraints.get("min_value", -math.inf), self.min_value) max_bound = min(constraints.get("max_value", math.inf), self.max_value) # Adjustments for allow_subnormal=False, if any need to be made if -self.smallest_nonzero_magnitude < min_bound < 0: min_bound = -0.0 elif 0 < min_bound < self.smallest_nonzero_magnitude: min_bound = self.smallest_nonzero_magnitude if -self.smallest_nonzero_magnitude < max_bound < 0: max_bound = -self.smallest_nonzero_magnitude elif 0 < max_bound < self.smallest_nonzero_magnitude: max_bound = 0.0 if min_bound > max_bound: return nothing() if ( min_bound > self.min_value or self.max_value > max_bound or (self.allow_nan and (-math.inf < min_bound or max_bound < math.inf)) ): self = type(self)( min_value=min_bound, max_value=max_bound, allow_nan=False, smallest_nonzero_magnitude=self.smallest_nonzero_magnitude, ) if pred is None: return self return super().filter(pred) @cacheable @defines_strategy(force_reusable_values=True) def floats( min_value: Real | None = None, max_value: Real | None = None, *, allow_nan: bool | None = None, allow_infinity: bool | None = None, allow_subnormal: bool | None = None, width: Literal[16, 32, 64] = 64, exclude_min: bool = False, exclude_max: bool = False, ) -> SearchStrategy[float]: """Returns a strategy which generates floats. - If min_value is not None, all values will be ``>= min_value`` (or ``> min_value`` if ``exclude_min``). - If max_value is not None, all values will be ``<= max_value`` (or ``< max_value`` if ``exclude_max``). - If min_value or max_value is not None, it is an error to enable allow_nan. - If both min_value and max_value are not None, it is an error to enable allow_infinity. - If inferred values range does not include subnormal values, it is an error to enable allow_subnormal. Where not explicitly ruled out by the bounds, :wikipedia:`subnormals `, infinities, and NaNs are possible values generated by this strategy. The width argument specifies the maximum number of bits of precision required to represent the generated float. Valid values are 16, 32, or 64. Passing ``width=32`` will still use the builtin 64-bit :class:`~python:float` class, but always for values which can be exactly represented as a 32-bit float. The exclude_min and exclude_max argument can be used to generate numbers from open or half-open intervals, by excluding the respective endpoints. Excluding either signed zero will also exclude the other. Attempting to exclude an endpoint which is None will raise an error; use ``allow_infinity=False`` to generate finite floats. You can however use e.g. ``min_value=-math.inf, exclude_min=True`` to exclude only one infinite endpoint. Examples from this strategy have a complicated and hard to explain shrinking behaviour, but it tries to improve "human readability". Finite numbers will be preferred to infinity and infinity will be preferred to NaN. """ check_type(bool, exclude_min, "exclude_min") check_type(bool, exclude_max, "exclude_max") if allow_nan is None: allow_nan = bool(min_value is None and max_value is None) elif allow_nan and (min_value is not None or max_value is not None): raise InvalidArgument(f"Cannot have {allow_nan=}, with min_value or max_value") if width not in (16, 32, 64): raise InvalidArgument( f"Got {width=}, but the only valid values " "are the integers 16, 32, and 64." ) # Literal[16] accepts both 16 and 16.0. Normalize to the int 16 here, mainly # for mypyc. We want to support width=16.0 to make e.g. width=mywidth / 2 for # mywidth=32 easy. width = cast(Literal[16, 32, 64], int(width)) check_valid_bound(min_value, "min_value") check_valid_bound(max_value, "max_value") if math.copysign(1.0, -0.0) == 1.0: # pragma: no cover raise FloatingPointError( "Your Python install can't represent -0.0, which is required by the " "IEEE-754 floating-point specification. This is probably because it was " "compiled with an unsafe option like -ffast-math; for a more detailed " "explanation see https://simonbyrne.github.io/notes/fastmath/" ) if allow_subnormal and next_up(0.0, width=width) == 0: # pragma: no cover # Not worth having separate CI envs and dependencies just to cover this branch; # discussion in https://github.com/HypothesisWorks/hypothesis/issues/3092 # # Erroring out here ensures that the database contents are interpreted # consistently - which matters for such a foundational strategy, even if it's # not always true for all user-composed strategies further up the stack. from _hypothesis_ftz_detector import identify_ftz_culprits try: ftz_pkg = identify_ftz_culprits() except Exception: ftz_pkg = None if ftz_pkg: ftz_msg = ( f"This seems to be because the `{ftz_pkg}` package was compiled with " f"-ffast-math or a similar option, which sets global processor state " f"- see https://simonbyrne.github.io/notes/fastmath/ for details. " f"If you don't know why {ftz_pkg} is installed, `pipdeptree -rp " f"{ftz_pkg}` will show which packages depend on it." ) else: ftz_msg = ( "This is usually because something was compiled with -ffast-math " "or a similar option, which sets global processor state. See " "https://simonbyrne.github.io/notes/fastmath/ for a more detailed " "writeup - and good luck!" ) raise FloatingPointError( f"Got {allow_subnormal=}, but we can't represent " f"subnormal floats right now, in violation of the IEEE-754 floating-point " f"specification. {ftz_msg}" ) min_arg, max_arg = min_value, max_value if min_value is not None: min_value = float_of(min_value, width) assert isinstance(min_value, float) if max_value is not None: max_value = float_of(max_value, width) assert isinstance(max_value, float) if min_value != min_arg: raise InvalidArgument( f"min_value={min_arg!r} cannot be exactly represented as a float " f"of width {width} - use {min_value=} instead." ) if max_value != max_arg: raise InvalidArgument( f"max_value={max_arg!r} cannot be exactly represented as a float " f"of width {width} - use {max_value=} instead." ) if exclude_min and (min_value is None or min_value == math.inf): raise InvalidArgument(f"Cannot exclude {min_value=}") if exclude_max and (max_value is None or max_value == -math.inf): raise InvalidArgument(f"Cannot exclude {max_value=}") assumed_allow_subnormal = allow_subnormal is None or allow_subnormal if min_value is not None and ( exclude_min or (min_arg is not None and min_value < min_arg) ): min_value = next_up_normal( min_value, width, allow_subnormal=assumed_allow_subnormal ) if min_value == min_arg: assert min_value == min_arg == 0 assert is_negative(min_arg) assert not is_negative(min_value) min_value = next_up_normal( min_value, width, allow_subnormal=assumed_allow_subnormal ) assert min_value > min_arg # type: ignore if max_value is not None and ( exclude_max or (max_arg is not None and max_value > max_arg) ): max_value = next_down_normal( max_value, width, allow_subnormal=assumed_allow_subnormal ) if max_value == max_arg: assert max_value == max_arg == 0 assert is_negative(max_value) assert not is_negative(max_arg) max_value = next_down_normal( max_value, width, allow_subnormal=assumed_allow_subnormal ) assert max_value < max_arg # type: ignore if min_value == -math.inf: min_value = None if max_value == math.inf: max_value = None bad_zero_bounds = ( min_value == max_value == 0 and is_negative(max_value) and not is_negative(min_value) ) if ( min_value is not None and max_value is not None and (min_value > max_value or bad_zero_bounds) ): # This is a custom alternative to check_valid_interval, because we want # to include the bit-width and exclusion information in the message. msg = ( f"There are no {width}-bit floating-point values between " f"min_value={min_arg!r} and max_value={max_arg!r}" ) if exclude_min or exclude_max: msg += f", {exclude_min=} and {exclude_max=}" raise InvalidArgument(msg) if allow_infinity is None: allow_infinity = bool(min_value is None or max_value is None) elif allow_infinity: if min_value is not None and max_value is not None: raise InvalidArgument( f"Cannot have {allow_infinity=}, with both min_value and max_value" ) elif min_value == math.inf: if min_arg == math.inf: raise InvalidArgument("allow_infinity=False excludes min_value=inf") raise InvalidArgument( f"exclude_min=True turns min_value={min_arg!r} into inf, " "but allow_infinity=False" ) elif max_value == -math.inf: if max_arg == -math.inf: raise InvalidArgument("allow_infinity=False excludes max_value=-inf") raise InvalidArgument( f"exclude_max=True turns max_value={max_arg!r} into -inf, " "but allow_infinity=False" ) smallest_normal = width_smallest_normals[width] if allow_subnormal is None: if min_value is not None and max_value is not None: if min_value == max_value: allow_subnormal = -smallest_normal < min_value < smallest_normal else: allow_subnormal = ( min_value < smallest_normal and max_value > -smallest_normal ) elif min_value is not None: allow_subnormal = min_value < smallest_normal elif max_value is not None: allow_subnormal = max_value > -smallest_normal else: allow_subnormal = True if allow_subnormal: if min_value is not None and min_value >= smallest_normal: raise InvalidArgument( f"allow_subnormal=True, but minimum value {min_value} " f"excludes values below float{width}'s " f"smallest positive normal {smallest_normal}" ) if max_value is not None and max_value <= -smallest_normal: raise InvalidArgument( f"allow_subnormal=True, but maximum value {max_value} " f"excludes values above float{width}'s " f"smallest negative normal {-smallest_normal}" ) if min_value is None: min_value = float("-inf") if max_value is None: max_value = float("inf") if not allow_infinity: min_value = max(min_value, next_up(float("-inf"))) max_value = min(max_value, next_down(float("inf"))) assert isinstance(min_value, float) assert isinstance(max_value, float) smallest_nonzero_magnitude = ( SMALLEST_SUBNORMAL if allow_subnormal else smallest_normal ) result: SearchStrategy = FloatStrategy( min_value=min_value, max_value=max_value, allow_nan=allow_nan, smallest_nonzero_magnitude=smallest_nonzero_magnitude, ) if width < 64: def downcast(x: float) -> float: try: return float_of(x, width) except OverflowError: # pragma: no cover reject() result = result.map(downcast) return result class NanStrategy(SearchStrategy[float]): """Strategy for sampling the space of nan float values.""" def do_draw(self, data: ConjectureData) -> float: # Nans must have all exponent bits and the first mantissa bit set, so # we generate by taking 64 random bits and setting the required ones. sign_bit = int(data.draw_boolean()) << 63 nan_bits = float_to_int(math.nan) mantissa_bits = data.draw_integer(0, 2**52 - 1) return int_to_float(sign_bit | nan_bits | mantissa_bits) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/random.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import abc import inspect import math from dataclasses import dataclass, field from random import Random from typing import Any from hypothesis.control import should_note from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.reflection import define_function_signature from hypothesis.reporting import report from hypothesis.strategies._internal.core import lists, permutations, sampled_from from hypothesis.strategies._internal.numbers import floats, integers from hypothesis.strategies._internal.strategies import SearchStrategy class HypothesisRandom(Random, abc.ABC): """A subclass of Random designed to expose the seed it was initially provided with.""" def __init__(self, *, note_method_calls: bool) -> None: self._note_method_calls = note_method_calls def __deepcopy__(self, table): return self.__copy__() @abc.abstractmethod def seed(self, seed): raise NotImplementedError @abc.abstractmethod def getstate(self): raise NotImplementedError @abc.abstractmethod def setstate(self, state): raise NotImplementedError @abc.abstractmethod def _hypothesis_do_random(self, method, kwargs): raise NotImplementedError def _hypothesis_log_random(self, method, kwargs, result): if not (self._note_method_calls and should_note()): return args, kwargs = convert_kwargs(method, kwargs) argstr = ", ".join( list(map(repr, args)) + [f"{k}={v!r}" for k, v in kwargs.items()] ) report(f"{self!r}.{method}({argstr}) -> {result!r}") RANDOM_METHODS = [ name for name in [ "_randbelow", "betavariate", "binomialvariate", "choice", "choices", "expovariate", "gammavariate", "gauss", "getrandbits", "lognormvariate", "normalvariate", "paretovariate", "randint", "random", "randrange", "sample", "shuffle", "triangular", "uniform", "vonmisesvariate", "weibullvariate", "randbytes", ] if hasattr(Random, name) ] # Fake shims to get a good signature def getrandbits(self, n: int) -> int: # type: ignore raise NotImplementedError def random(self) -> float: # type: ignore raise NotImplementedError def _randbelow(self, n: int) -> int: # type: ignore raise NotImplementedError STUBS = {f.__name__: f for f in [getrandbits, random, _randbelow]} SIGNATURES: dict[str, inspect.Signature] = {} def sig_of(name): try: return SIGNATURES[name] except KeyError: pass target = getattr(Random, name) result = inspect.signature(STUBS.get(name, target)) SIGNATURES[name] = result return result def define_copy_method(name): target = getattr(Random, name) def implementation(self, **kwargs): result = self._hypothesis_do_random(name, kwargs) self._hypothesis_log_random(name, kwargs, result) return result sig = inspect.signature(STUBS.get(name, target)) result = define_function_signature(target.__name__, target.__doc__, sig)( implementation ) result.__module__ = __name__ result.__qualname__ = "HypothesisRandom." + result.__name__ setattr(HypothesisRandom, name, result) for r in RANDOM_METHODS: define_copy_method(r) @dataclass(slots=True, frozen=False) class RandomState: next_states: dict = field(default_factory=dict) state_id: Any = None def state_for_seed(data, seed): if data.seeds_to_states is None: data.seeds_to_states = {} seeds_to_states = data.seeds_to_states try: state = seeds_to_states[seed] except KeyError: state = RandomState() seeds_to_states[seed] = state return state def normalize_zero(f: float) -> float: if f == 0.0: return 0.0 else: return f class ArtificialRandom(HypothesisRandom): VERSION = 10**6 def __init__(self, *, note_method_calls: bool, data: ConjectureData) -> None: super().__init__(note_method_calls=note_method_calls) self.__data = data self.__state = RandomState() def __repr__(self) -> str: return "HypothesisRandom(generated data)" def __copy__(self) -> "ArtificialRandom": result = ArtificialRandom( note_method_calls=self._note_method_calls, data=self.__data, ) result.setstate(self.getstate()) return result def __convert_result(self, method, kwargs, result): if method == "choice": return kwargs.get("seq")[result] if method in ("choices", "sample"): seq = kwargs["population"] return [seq[i] for i in result] if method == "shuffle": seq = kwargs["x"] original = list(seq) for i, i2 in enumerate(result): seq[i] = original[i2] return None return result def _hypothesis_do_random(self, method, kwargs): if method == "choices": key = (method, len(kwargs["population"]), kwargs.get("k")) elif method == "choice": key = (method, len(kwargs["seq"])) elif method == "shuffle": key = (method, len(kwargs["x"])) else: key = (method, *sorted(kwargs)) try: result, self.__state = self.__state.next_states[key] except KeyError: pass else: return self.__convert_result(method, kwargs, result) if method == "_randbelow": result = self.__data.draw_integer(0, kwargs["n"] - 1) elif method == "random": # See https://github.com/HypothesisWorks/hypothesis/issues/4297 # for numerics/bounds of "random" and "betavariate" result = self.__data.draw(floats(0, 1, exclude_max=True)) elif method == "betavariate": result = self.__data.draw(floats(0, 1)) elif method == "uniform": a = normalize_zero(kwargs["a"]) b = normalize_zero(kwargs["b"]) result = self.__data.draw(floats(a, b)) elif method in ("weibullvariate", "gammavariate"): result = self.__data.draw(floats(min_value=0.0, allow_infinity=False)) elif method in ("gauss", "normalvariate"): mu = kwargs["mu"] result = mu + self.__data.draw( floats(allow_nan=False, allow_infinity=False) ) elif method == "vonmisesvariate": result = self.__data.draw(floats(0, 2 * math.pi)) elif method == "randrange": if kwargs["stop"] is None: stop = kwargs["start"] start = 0 else: start = kwargs["start"] stop = kwargs["stop"] step = kwargs["step"] if start == stop: raise ValueError(f"empty range for randrange({start}, {stop}, {step})") if step != 1: endpoint = (stop - start) // step if (start - stop) % step == 0: endpoint -= 1 i = self.__data.draw_integer(0, endpoint) result = start + i * step else: result = self.__data.draw_integer(start, stop - 1) elif method == "randint": result = self.__data.draw_integer(kwargs["a"], kwargs["b"]) # New in Python 3.12, so not taken by our coverage job elif method == "binomialvariate": # pragma: no cover result = self.__data.draw_integer(0, kwargs["n"]) elif method == "choice": seq = kwargs["seq"] result = self.__data.draw_integer(0, len(seq) - 1) elif method == "choices": k = kwargs["k"] result = self.__data.draw( lists( integers(0, len(kwargs["population"]) - 1), min_size=k, max_size=k, ) ) elif method == "sample": k = kwargs["k"] seq = kwargs["population"] if k > len(seq) or k < 0: raise ValueError( f"Sample size {k} not in expected range 0 <= k <= {len(seq)}" ) if k == 0: result = [] else: result = self.__data.draw( lists( sampled_from(range(len(seq))), min_size=k, max_size=k, unique=True, ) ) elif method == "getrandbits": result = self.__data.draw_integer(0, 2 ** kwargs["n"] - 1) elif method == "triangular": low = normalize_zero(kwargs["low"]) high = normalize_zero(kwargs["high"]) mode = normalize_zero(kwargs["mode"]) if mode is None: result = self.__data.draw(floats(low, high)) elif self.__data.draw_boolean(0.5): result = self.__data.draw(floats(mode, high)) else: result = self.__data.draw(floats(low, mode)) elif method in ("paretovariate", "expovariate", "lognormvariate"): result = self.__data.draw(floats(min_value=0.0)) elif method == "shuffle": result = self.__data.draw(permutations(range(len(kwargs["x"])))) elif method == "randbytes": n = int(kwargs["n"]) result = self.__data.draw_bytes(min_size=n, max_size=n) else: raise NotImplementedError(method) new_state = RandomState() self.__state.next_states[key] = (result, new_state) self.__state = new_state return self.__convert_result(method, kwargs, result) def seed(self, seed): self.__state = state_for_seed(self.__data, seed) def getstate(self): if self.__state.state_id is not None: return self.__state.state_id if self.__data.states_for_ids is None: self.__data.states_for_ids = {} states_for_ids = self.__data.states_for_ids self.__state.state_id = len(states_for_ids) states_for_ids[self.__state.state_id] = self.__state return self.__state.state_id def setstate(self, state): self.__state = self.__data.states_for_ids[state] DUMMY_RANDOM = Random(0) def convert_kwargs(name, kwargs): kwargs = dict(kwargs) signature = sig_of(name) params = signature.parameters bound = signature.bind(DUMMY_RANDOM, **kwargs) bound.apply_defaults() for k in list(kwargs): if ( kwargs[k] is params[k].default or params[k].kind != inspect.Parameter.KEYWORD_ONLY ): kwargs.pop(k) arg_names = list(params)[1:] args = [] for a in arg_names: if params[a].kind == inspect.Parameter.KEYWORD_ONLY: break args.append(bound.arguments[a]) kwargs.pop(a, None) while args: name = arg_names[len(args) - 1] if args[-1] is params[name].default: args.pop() else: break return (args, kwargs) class TrueRandom(HypothesisRandom): def __init__(self, seed, note_method_calls): super().__init__(note_method_calls=note_method_calls) self.__seed = seed self.__random = Random(seed) def _hypothesis_do_random(self, method, kwargs): fn = getattr(self.__random, method) try: return fn(**kwargs) except TypeError: pass args, kwargs = convert_kwargs(method, kwargs) return fn(*args, **kwargs) def __copy__(self) -> "TrueRandom": result = TrueRandom( seed=self.__seed, note_method_calls=self._note_method_calls, ) result.setstate(self.getstate()) return result def __repr__(self) -> str: return f"Random({self.__seed!r})" def seed(self, seed): self.__random.seed(seed) self.__seed = seed def getstate(self): return self.__random.getstate() def setstate(self, state): self.__random.setstate(state) class RandomStrategy(SearchStrategy[HypothesisRandom]): def __init__(self, *, note_method_calls: bool, use_true_random: bool) -> None: super().__init__() self.__note_method_calls = note_method_calls self.__use_true_random = use_true_random def do_draw(self, data: ConjectureData) -> HypothesisRandom: if self.__use_true_random: seed = data.draw_integer(0, 2**64 - 1) return TrueRandom(seed=seed, note_method_calls=self.__note_method_calls) else: return ArtificialRandom( note_method_calls=self.__note_method_calls, data=data ) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/recursive.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import threading import warnings from collections.abc import Callable from contextlib import contextmanager from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.internal.reflection import ( get_pretty_function_description, is_first_param_referenced_in_function, is_identity_function, ) from hypothesis.internal.validation import check_type from hypothesis.strategies._internal.strategies import ( OneOfStrategy, SearchStrategy, check_strategy, ) from hypothesis.utils.deprecation import note_deprecation class LimitReached(BaseException): pass class LimitedStrategy(SearchStrategy): def __init__(self, strategy): super().__init__() self.base_strategy = strategy self._threadlocal = threading.local() @property def marker(self): return getattr(self._threadlocal, "marker", 0) @marker.setter def marker(self, value): self._threadlocal.marker = value @property def currently_capped(self): return getattr(self._threadlocal, "currently_capped", False) @currently_capped.setter def currently_capped(self, value): self._threadlocal.currently_capped = value def __repr__(self) -> str: return f"LimitedStrategy({self.base_strategy!r})" def do_validate(self) -> None: self.base_strategy.validate() def do_draw(self, data): assert self.currently_capped if self.marker <= 0: raise LimitReached self.marker -= 1 return data.draw(self.base_strategy) @contextmanager def capped(self, max_templates): try: was_capped = self.currently_capped self.currently_capped = True self.marker = max_templates yield finally: self.currently_capped = was_capped class RecursiveStrategy(SearchStrategy): def __init__( self, base: SearchStrategy, extend: Callable[[SearchStrategy], SearchStrategy], min_leaves: int | None, max_leaves: int, ): super().__init__() self.min_leaves = min_leaves self.max_leaves = max_leaves self.base = base self.limited_base = LimitedStrategy(base) self.extend = extend strategies = [self.limited_base, self.extend(self.limited_base)] while 2 ** (len(strategies) - 1) <= max_leaves: strategies.append(extend(OneOfStrategy(tuple(strategies)))) # If min_leaves > 1, we can never draw from base directly if min_leaves is not None and min_leaves > 1: strategies = strategies[1:] self.strategy = OneOfStrategy(strategies) def __repr__(self) -> str: if not hasattr(self, "_cached_repr"): self._cached_repr = ( f"recursive({self.base!r}, " f"{get_pretty_function_description(self.extend)}, " f"min_leaves={self.min_leaves}, max_leaves={self.max_leaves})" ) return self._cached_repr def do_validate(self) -> None: check_strategy(self.base, "base") extended = self.extend(self.limited_base) check_strategy(extended, f"extend({self.limited_base!r})") self.limited_base.validate() extended.validate() if is_identity_function(self.extend): warnings.warn( "extend=lambda x: x is a no-op; you probably want to use a " "different extend function, or just use the base strategy directly.", HypothesisWarning, stacklevel=5, ) if not is_first_param_referenced_in_function(self.extend): msg = ( f"extend={get_pretty_function_description(self.extend)} doesn't use " "it's argument, and thus can't actually recurse!" ) if self.min_leaves is None: note_deprecation( msg, since="2026-01-12", has_codemod=False, stacklevel=1, ) else: raise InvalidArgument(msg) if self.min_leaves is not None: check_type(int, self.min_leaves, "min_leaves") check_type(int, self.max_leaves, "max_leaves") if self.min_leaves is not None and self.min_leaves <= 0: raise InvalidArgument( f"min_leaves={self.min_leaves!r} must be greater than zero" ) if self.max_leaves <= 0: raise InvalidArgument( f"max_leaves={self.max_leaves!r} must be greater than zero" ) if (self.min_leaves or 1) > self.max_leaves: raise InvalidArgument( f"min_leaves={self.min_leaves!r} must be less than or equal to " f"max_leaves={self.max_leaves!r}" ) def do_draw(self, data): min_leaves_retries = 0 while True: try: with self.limited_base.capped(self.max_leaves): result = data.draw(self.strategy) leaves_drawn = self.max_leaves - self.limited_base.marker if self.min_leaves and leaves_drawn < self.min_leaves: data.events[ f"Draw for {self!r} had fewer than " f"min_leaves={self.min_leaves} and had to be retried" ] = "" min_leaves_retries += 1 if min_leaves_retries < 5: continue data.mark_invalid(f"min_leaves={self.min_leaves} unsatisfied") return result except LimitReached: data.events[ f"Draw for {self!r} exceeded " f"max_leaves={self.max_leaves} and had to be retried" ] = "" ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/regex.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import operator import re from hypothesis.errors import InvalidArgument from hypothesis.internal import charmap from hypothesis.strategies._internal.lazy import unwrap_strategies from hypothesis.strategies._internal.strings import OneCharStringStrategy try: # pragma: no cover import re._constants as sre import re._parser as sre_parse ATOMIC_GROUP = sre.ATOMIC_GROUP POSSESSIVE_REPEAT = sre.POSSESSIVE_REPEAT except ImportError: # Python < 3.11 import sre_constants as sre import sre_parse ATOMIC_GROUP = object() POSSESSIVE_REPEAT = object() from hypothesis import reject, strategies as st from hypothesis.internal.charmap import as_general_categories, categories from hypothesis.internal.compat import add_note, int_to_byte UNICODE_CATEGORIES = set(categories()) SPACE_CHARS = set(" \t\n\r\f\v") UNICODE_SPACE_CHARS = SPACE_CHARS | set("\x1c\x1d\x1e\x1f\x85") UNICODE_DIGIT_CATEGORIES = {"Nd"} UNICODE_SPACE_CATEGORIES = set(as_general_categories(["Z"])) UNICODE_LETTER_CATEGORIES = set(as_general_categories(["L"])) UNICODE_WORD_CATEGORIES = set(as_general_categories(["L", "N"])) # This is verbose, but correct on all versions of Python BYTES_ALL = {int_to_byte(i) for i in range(256)} BYTES_DIGIT = {b for b in BYTES_ALL if re.match(b"\\d", b)} BYTES_SPACE = {b for b in BYTES_ALL if re.match(b"\\s", b)} BYTES_WORD = {b for b in BYTES_ALL if re.match(b"\\w", b)} BYTES_LOOKUP = { sre.CATEGORY_DIGIT: BYTES_DIGIT, sre.CATEGORY_SPACE: BYTES_SPACE, sre.CATEGORY_WORD: BYTES_WORD, sre.CATEGORY_NOT_DIGIT: BYTES_ALL - BYTES_DIGIT, sre.CATEGORY_NOT_SPACE: BYTES_ALL - BYTES_SPACE, sre.CATEGORY_NOT_WORD: BYTES_ALL - BYTES_WORD, } GROUP_CACHE_STRATEGY: st.SearchStrategy[dict] = st.shared( st.builds(dict), key="hypothesis.regex.group_cache" ) class IncompatibleWithAlphabet(InvalidArgument): pass @st.composite def update_group(draw, group_name, strategy): cache = draw(GROUP_CACHE_STRATEGY) result = draw(strategy) cache[group_name] = result return result @st.composite def reuse_group(draw, group_name): cache = draw(GROUP_CACHE_STRATEGY) try: return cache[group_name] except KeyError: reject() @st.composite def group_conditional(draw, group_name, if_yes, if_no): cache = draw(GROUP_CACHE_STRATEGY) if group_name in cache: return draw(if_yes) else: return draw(if_no) @st.composite def clear_cache_after_draw(draw, base_strategy): cache = draw(GROUP_CACHE_STRATEGY) result = draw(base_strategy) cache.clear() return result def chars_not_in_alphabet(alphabet, string): # Given a string, return a tuple of the characters which are not in alphabet if alphabet is None: return () intset = unwrap_strategies(alphabet).intervals return tuple(c for c in string if c not in intset) class Context: __slots__ = ["flags"] def __init__(self, flags): self.flags = flags class CharactersBuilder: """Helper object that allows to configure `characters` strategy with various unicode categories and characters. Also allows negation of configured set. :param negate: If True, configure :func:`hypothesis.strategies.characters` to match anything other than configured character set :param flags: Regex flags. They affect how and which characters are matched """ def __init__(self, *, negate=False, flags=0, alphabet): self._categories = set() self._whitelist_chars = set() self._blacklist_chars = set() self._negate = negate self._ignorecase = flags & re.IGNORECASE self.code_to_char = chr self._alphabet = unwrap_strategies(alphabet) if flags & re.ASCII: self._alphabet = OneCharStringStrategy( self._alphabet.intervals & charmap.query(max_codepoint=127) ) @property def strategy(self): """Returns resulting strategy that generates configured char set.""" # Start by getting the set of all characters allowed by the pattern white_chars = self._whitelist_chars - self._blacklist_chars multi_chars = {c for c in white_chars if len(c) > 1} intervals = charmap.query( categories=self._categories, exclude_characters=self._blacklist_chars, include_characters=white_chars - multi_chars, ) # Then take the complement if this is from a negated character class if self._negate: intervals = charmap.query() - intervals multi_chars.clear() # and finally return the intersection with our alphabet return OneCharStringStrategy(intervals & self._alphabet.intervals) | ( st.sampled_from(sorted(multi_chars)) if multi_chars else st.nothing() ) def add_category(self, category): """Update unicode state to match sre_parse object ``category``.""" if category == sre.CATEGORY_DIGIT: self._categories |= UNICODE_DIGIT_CATEGORIES elif category == sre.CATEGORY_NOT_DIGIT: self._categories |= UNICODE_CATEGORIES - UNICODE_DIGIT_CATEGORIES elif category == sre.CATEGORY_SPACE: self._categories |= UNICODE_SPACE_CATEGORIES self._whitelist_chars |= UNICODE_SPACE_CHARS elif category == sre.CATEGORY_NOT_SPACE: self._categories |= UNICODE_CATEGORIES - UNICODE_SPACE_CATEGORIES self._blacklist_chars |= UNICODE_SPACE_CHARS elif category == sre.CATEGORY_WORD: self._categories |= UNICODE_WORD_CATEGORIES self._whitelist_chars.add("_") elif category == sre.CATEGORY_NOT_WORD: self._categories |= UNICODE_CATEGORIES - UNICODE_WORD_CATEGORIES self._blacklist_chars.add("_") else: raise NotImplementedError(f"Unknown character category: {category}") def add_char(self, c): """Add given char to the whitelist.""" self._whitelist_chars.add(c) if ( self._ignorecase and re.match(re.escape(c), c.swapcase(), flags=re.IGNORECASE) is not None ): # Note that it is possible that `len(c.swapcase()) > 1` self._whitelist_chars.add(c.swapcase()) class BytesBuilder(CharactersBuilder): def __init__(self, *, negate=False, flags=0): self._whitelist_chars = set() self._blacklist_chars = set() self._negate = negate self._alphabet = None self._ignorecase = flags & re.IGNORECASE self.code_to_char = int_to_byte @property def strategy(self): """Returns resulting strategy that generates configured char set.""" allowed = self._whitelist_chars if self._negate: allowed = BYTES_ALL - allowed return st.sampled_from(sorted(allowed)) def add_category(self, category): """Update characters state to match sre_parse object ``category``.""" self._whitelist_chars |= BYTES_LOOKUP[category] @st.composite def maybe_pad(draw, regex, strategy, left_pad_strategy, right_pad_strategy): """Attempt to insert padding around the result of a regex draw while preserving the match.""" result = draw(strategy) left_pad = draw(left_pad_strategy) if left_pad and regex.search(left_pad + result): result = left_pad + result right_pad = draw(right_pad_strategy) if right_pad and regex.search(result + right_pad): result += right_pad return result def base_regex_strategy(regex, parsed=None, alphabet=None): if parsed is None: parsed = sre_parse.parse(regex.pattern, flags=regex.flags) try: s = _strategy( parsed, context=Context(flags=regex.flags), is_unicode=isinstance(regex.pattern, str), alphabet=alphabet, ) except Exception as err: add_note(err, f"{alphabet=} {regex=}") raise return clear_cache_after_draw(s) def regex_strategy( regex, fullmatch, *, alphabet, _temp_jsonschema_hack_no_end_newline=False ): if not hasattr(regex, "pattern"): regex = re.compile(regex) is_unicode = isinstance(regex.pattern, str) parsed = sre_parse.parse(regex.pattern, flags=regex.flags) if fullmatch: if not parsed: return st.just("" if is_unicode else b"") return base_regex_strategy(regex, parsed, alphabet).filter(regex.fullmatch) if not parsed: if is_unicode: return st.text(alphabet=alphabet) else: return st.binary() if is_unicode: base_padding_strategy = st.text(alphabet=alphabet) empty = st.just("") newline = st.just("\n") else: base_padding_strategy = st.binary() empty = st.just(b"") newline = st.just(b"\n") right_pad = base_padding_strategy left_pad = base_padding_strategy if parsed[-1][0] == sre.AT: if parsed[-1][1] == sre.AT_END_STRING: right_pad = empty elif parsed[-1][1] == sre.AT_END: if regex.flags & re.MULTILINE: right_pad = st.one_of( empty, st.builds(operator.add, newline, right_pad) ) else: right_pad = st.one_of(empty, newline) # This will be removed when a regex-syntax-translation library exists. # It's a pretty nasty hack, but means that we can match the semantics # of JSONschema's compatible subset of ECMA regex, which is important # for hypothesis-jsonschema and Schemathesis. See e.g. # https://github.com/schemathesis/schemathesis/issues/1241 if _temp_jsonschema_hack_no_end_newline: right_pad = empty if parsed[0][0] == sre.AT: if parsed[0][1] == sre.AT_BEGINNING_STRING: left_pad = empty elif parsed[0][1] == sre.AT_BEGINNING: if regex.flags & re.MULTILINE: left_pad = st.one_of(empty, st.builds(operator.add, left_pad, newline)) else: left_pad = empty base = base_regex_strategy(regex, parsed, alphabet).filter(regex.search) return maybe_pad(regex, base, left_pad, right_pad) def _strategy(codes, context, is_unicode, *, alphabet): """Convert SRE regex parse tree to strategy that generates strings matching that regex represented by that parse tree. `codes` is either a list of SRE regex elements representations or a particular element representation. Each element is a tuple of element code (as string) and parameters. E.g. regex 'ab[0-9]+' compiles to following elements: [ (LITERAL, 97), (LITERAL, 98), (MAX_REPEAT, (1, 4294967295, [ (IN, [ (RANGE, (48, 57)) ]) ])) ] The function recursively traverses regex element tree and converts each element to strategy that generates strings that match that element. Context stores 1. List of groups (for backreferences) 2. Active regex flags (e.g. IGNORECASE, DOTALL, UNICODE, they affect behavior of various inner strategies) """ def recurse(codes): return _strategy(codes, context, is_unicode, alphabet=alphabet) if is_unicode: empty = "" to_char = chr else: empty = b"" to_char = int_to_byte binary_char = st.binary(min_size=1, max_size=1) if not isinstance(codes, tuple): # List of codes strategies = [] i = 0 while i < len(codes): if codes[i][0] == sre.LITERAL and not context.flags & re.IGNORECASE: # Merge subsequent "literals" into one `just()` strategy # that generates corresponding text if no IGNORECASE j = i + 1 while j < len(codes) and codes[j][0] == sre.LITERAL: j += 1 if i + 1 < j: chars = empty.join(to_char(charcode) for _, charcode in codes[i:j]) if invalid := chars_not_in_alphabet(alphabet, chars): raise IncompatibleWithAlphabet( f"Literal {chars!r} contains characters {invalid!r} " f"which are not in the specified alphabet" ) strategies.append(st.just(chars)) i = j continue strategies.append(recurse(codes[i])) i += 1 # We handle this separately at the top level, but some regex can # contain empty lists internally, so we need to handle this here too. if not strategies: return st.just(empty) if len(strategies) == 1: return strategies[0] return st.tuples(*strategies).map(empty.join) else: # Single code code, value = codes if code == sre.LITERAL: # Regex 'a' (single char) c = to_char(value) if chars_not_in_alphabet(alphabet, c): raise IncompatibleWithAlphabet( f"Literal {c!r} is not in the specified alphabet" ) if ( context.flags & re.IGNORECASE and c != c.swapcase() and re.match(re.escape(c), c.swapcase(), re.IGNORECASE) is not None and not chars_not_in_alphabet(alphabet, c.swapcase()) ): # We do the explicit check for swapped-case matching because # eg 'ß'.upper() == 'SS' and ignorecase doesn't match it. return st.sampled_from([c, c.swapcase()]) return st.just(c) elif code == sre.NOT_LITERAL: # Regex '[^a]' (negation of a single char) c = to_char(value) blacklist = {c} if ( context.flags & re.IGNORECASE and re.match(re.escape(c), c.swapcase(), re.IGNORECASE) is not None ): # There are a few cases where .swapcase() returns two characters, # but is still a case-insensitive match. In such cases we add *both* # characters to our blacklist, to avoid doing the wrong thing for # patterns such as r"[^\u0130]+" where "i\u0307" matches. # # (that's respectively 'Latin letter capital I with dot above' and # 'latin latter i' + 'combining dot above'; see issue #2657) # # As a final additional wrinkle, "latin letter capital I" *also* # case-insensitive-matches, with or without combining dot character. # We therefore have to chain .swapcase() calls until a fixpoint. stack = [c.swapcase()] while stack: for char in stack.pop(): blacklist.add(char) stack.extend(set(char.swapcase()) - blacklist) if is_unicode: return OneCharStringStrategy( unwrap_strategies(alphabet).intervals & charmap.query(exclude_characters=blacklist) ) else: return binary_char.filter(lambda c: c not in blacklist) elif code == sre.IN: # Regex '[abc0-9]' (set of characters) negate = value[0][0] == sre.NEGATE if is_unicode: builder = CharactersBuilder( flags=context.flags, negate=negate, alphabet=alphabet ) else: builder = BytesBuilder(flags=context.flags, negate=negate) for charset_code, charset_value in value: if charset_code == sre.NEGATE: # Regex '[^...]' (negation) # handled by builder = CharactersBuilder(...) above pass elif charset_code == sre.LITERAL: # Regex '[a]' (single char) c = builder.code_to_char(charset_value) if chars_not_in_alphabet(builder._alphabet, c): raise IncompatibleWithAlphabet( f"Literal {c!r} is not in the specified alphabet" ) builder.add_char(c) elif charset_code == sre.RANGE: # Regex '[a-z]' (char range) low, high = charset_value chars = empty.join(map(builder.code_to_char, range(low, high + 1))) if len(chars) == len( invalid := set(chars_not_in_alphabet(alphabet, chars)) ): raise IncompatibleWithAlphabet( f"Charset '[{chr(low)}-{chr(high)}]' contains characters {invalid!r} " f"which are not in the specified alphabet" ) for c in chars: if isinstance(c, int): c = int_to_byte(c) if c not in invalid: builder.add_char(c) elif charset_code == sre.CATEGORY: # Regex '[\w]' (char category) builder.add_category(charset_value) else: # Currently there are no known code points other than # handled here. This code is just future proofing raise NotImplementedError(f"Unknown charset code: {charset_code}") return builder.strategy elif code == sre.ANY: # Regex '.' (any char) if is_unicode: assert alphabet is not None if context.flags & re.DOTALL: return alphabet return OneCharStringStrategy( unwrap_strategies(alphabet).intervals & charmap.query(exclude_characters="\n") ) else: if context.flags & re.DOTALL: return binary_char return binary_char.filter(lambda c: c != b"\n") elif code == sre.AT: # Regexes like '^...', '...$', '\bfoo', '\Bfoo' # An empty string (or newline) will match the token itself, but # we don't and can't check the position (eg '%' at the end) return st.just(empty) elif code == sre.SUBPATTERN: # Various groups: '(...)', '(:...)' or '(?P...)' old_flags = context.flags context.flags = (context.flags | value[1]) & ~value[2] strat = _strategy(value[-1], context, is_unicode, alphabet=alphabet) context.flags = old_flags if value[0]: strat = update_group(value[0], strat) return strat elif code == sre.GROUPREF: # Regex '\\1' or '(?P=name)' (group reference) return reuse_group(value) elif code == sre.ASSERT: # Regex '(?=...)' or '(?<=...)' (positive lookahead/lookbehind) return recurse(value[1]) elif code == sre.ASSERT_NOT: # Regex '(?!...)' or '(? str: if self.key is not None: return f"shared({self.base!r}, key={self.key!r})" else: return f"shared({self.base!r})" def calc_label(self) -> int: return self.base.calc_label() # Ideally would be -> Ex, but key collisions with different-typed values are # possible. See https://github.com/HypothesisWorks/hypothesis/issues/4301. def do_draw(self, data: ConjectureData) -> Any: key = self.key or self if key not in data._shared_strategy_draws: drawn = data.draw(self.base) data._shared_strategy_draws[key] = (drawn, self) else: drawn, other = data._shared_strategy_draws[key] # Check that the strategies shared under this key are equivalent if self.label != other.label: warnings.warn( f"Different strategies are shared under {key=}. This" " risks drawing values that are not valid examples for the strategy," " or that have a narrower range than expected." f" Conflicting strategies: ({self!r}, {other!r}).", HypothesisWarning, stacklevel=1, ) return drawn ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/strategies.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import sys import threading import warnings from collections import abc, defaultdict from collections.abc import Callable, Sequence from functools import lru_cache from random import shuffle from threading import RLock from typing import ( TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeAlias, TypeVar, cast, overload, ) from hypothesis._settings import HealthCheck, Phase, Verbosity, settings from hypothesis.control import _current_build_context, current_build_context from hypothesis.errors import ( HypothesisException, HypothesisWarning, InvalidArgument, NonInteractiveExampleWarning, UnsatisfiedAssumption, ) from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.utils import ( calc_label_from_cls, calc_label_from_hash, calc_label_from_name, combine_labels, ) from hypothesis.internal.coverage import check_function from hypothesis.internal.reflection import ( get_pretty_function_description, is_identity_function, ) from hypothesis.strategies._internal.utils import defines_strategy from hypothesis.utils.conventions import UniqueIdentifier if TYPE_CHECKING: Ex = TypeVar("Ex", covariant=True, default=Any) else: Ex = TypeVar("Ex", covariant=True) T = TypeVar("T") T3 = TypeVar("T3") T4 = TypeVar("T4") T5 = TypeVar("T5") MappedFrom = TypeVar("MappedFrom") MappedTo = TypeVar("MappedTo") RecurT: TypeAlias = Callable[["SearchStrategy"], bool] calculating = UniqueIdentifier("calculating") MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL = calc_label_from_name( "another attempted draw in MappedStrategy" ) FILTERED_SEARCH_STRATEGY_DO_DRAW_LABEL = calc_label_from_name( "single loop iteration in FilteredStrategy" ) label_lock = RLock() def recursive_property(strategy: "SearchStrategy", name: str, default: object) -> Any: """Handle properties which may be mutually recursive among a set of strategies. These are essentially lazily cached properties, with the ability to set an override: If the property has not been explicitly set, we calculate it on first access and memoize the result for later. The problem is that for properties that depend on each other, a naive calculation strategy may hit infinite recursion. Consider for example the property is_empty. A strategy defined as x = st.deferred(lambda: x) is certainly empty (in order to draw a value from x we would have to draw a value from x, for which we would have to draw a value from x, ...), but in order to calculate it the naive approach would end up calling x.is_empty in order to calculate x.is_empty in order to etc. The solution is one of fixed point calculation. We start with a default value that is the value of the property in the absence of evidence to the contrary, and then update the values of the property for all dependent strategies until we reach a fixed point. The approach taken roughly follows that in section 4.2 of Adams, Michael D., Celeste Hollenbeck, and Matthew Might. "On the complexity and performance of parsing with derivatives." ACM SIGPLAN Notices 51.6 (2016): 224-236. """ assert name in {"is_empty", "has_reusable_values", "is_cacheable"} cache_key = "cached_" + name calculation = "calc_" + name force_key = "force_" + name def forced_value(target: SearchStrategy) -> Any: try: return getattr(target, force_key) except AttributeError: return getattr(target, cache_key) try: return forced_value(strategy) except AttributeError: pass mapping: dict[SearchStrategy, Any] = {} sentinel = object() hit_recursion = False # For a first pass we do a direct recursive calculation of the # property, but we block recursively visiting a value in the # computation of its property: When that happens, we simply # note that it happened and return the default value. def recur(strat: SearchStrategy) -> Any: nonlocal hit_recursion try: return forced_value(strat) except AttributeError: pass result = mapping.get(strat, sentinel) if result is calculating: hit_recursion = True return default elif result is sentinel: mapping[strat] = calculating mapping[strat] = getattr(strat, calculation)(recur) return mapping[strat] return result recur(strategy) # If we hit self-recursion in the computation of any strategy # value, our mapping at the end is imprecise - it may or may # not have the right values in it. We now need to proceed with # a more careful fixed point calculation to get the exact # values. Hopefully our mapping is still pretty good and it # won't take a large number of updates to reach a fixed point. if hit_recursion: needs_update = set(mapping) # We track which strategies use which in the course of # calculating their property value. If A ever uses B in # the course of calculating its value, then whenever the # value of B changes we might need to update the value of # A. listeners: dict[SearchStrategy, set[SearchStrategy]] = defaultdict(set) else: needs_update = None def recur2(strat: SearchStrategy) -> Any: def recur_inner(other: SearchStrategy) -> Any: try: return forced_value(other) except AttributeError: pass listeners[other].add(strat) result = mapping.get(other, sentinel) if result is sentinel: assert needs_update is not None needs_update.add(other) mapping[other] = default return default return result return recur_inner count = 0 seen = set() while needs_update: count += 1 # If we seem to be taking a really long time to stabilize we # start tracking seen values to attempt to detect an infinite # loop. This should be impossible, and most code will never # hit the count, but having an assertion for it means that # testing is easier to debug and we don't just have a hung # test. # Note: This is actually covered, by test_very_deep_deferral # in tests/cover/test_deferred_strategies.py. Unfortunately it # runs into a coverage bug. See # https://github.com/nedbat/coveragepy/issues/605 # for details. if count > 50: # pragma: no cover key = frozenset(mapping.items()) assert key not in seen, (key, name) seen.add(key) to_update = needs_update needs_update = set() for strat in to_update: new_value = getattr(strat, calculation)(recur2(strat)) if new_value != mapping[strat]: needs_update.update(listeners[strat]) mapping[strat] = new_value # We now have a complete and accurate calculation of the # property values for everything we have seen in the course of # running this calculation. We simultaneously update all of # them (not just the strategy we started out with). for k, v in mapping.items(): setattr(k, cache_key, v) return getattr(strategy, cache_key) class SearchStrategy(Generic[Ex]): """A ``SearchStrategy`` tells Hypothesis how to generate that kind of input. This class is only part of the public API for use in type annotations, so that you can write e.g. ``-> SearchStrategy[Foo]`` for your function which returns ``builds(Foo, ...)``. Do not inherit from or directly instantiate this class. """ __module__: str = "hypothesis.strategies" LABELS: ClassVar[dict[type, int]] = {} # triggers `assert isinstance(label, int)` under threading when setting this # in init instead of a classvar. I'm not sure why, init should be safe. But # this works so I'm not looking into it further atm. __label: int | UniqueIdentifier | None = None def __init__(self): self.validate_called: dict[int, bool] = {} def is_currently_empty(self, data: ConjectureData) -> bool: """ Returns whether this strategy is currently empty. Unlike ``empty``, which is computed based on static information and cannot change, ``is_currently_empty`` may change over time based on choices made during the test case. This is currently only used for stateful testing, where |Bundle| grows a list of values to choose from over the course of a test case. ``data`` will only be used for introspection. No values will be drawn from it in a way that modifies the choice sequence. """ return self.is_empty @property def is_empty(self) -> Any: # Returns True if this strategy can never draw a value and will always # result in the data being marked invalid. # The fact that this returns False does not guarantee that a valid value # can be drawn - this is not intended to be perfect, and is primarily # intended to be an optimisation for some cases. return recursive_property(self, "is_empty", True) # Returns True if values from this strategy can safely be reused without # this causing unexpected behaviour. # True if values from this strategy can be implicitly reused (e.g. as # background values in a numpy array) without causing surprising # user-visible behaviour. Should be false for built-in strategies that # produce mutable values, and for strategies that have been mapped/filtered # by arbitrary user-provided functions. @property def has_reusable_values(self) -> Any: return recursive_property(self, "has_reusable_values", True) @property def is_cacheable(self) -> Any: """ Whether it is safe to hold on to instances of this strategy in a cache. See _STRATEGY_CACHE. """ return recursive_property(self, "is_cacheable", True) def calc_is_cacheable(self, recur: RecurT) -> bool: return True def calc_is_empty(self, recur: RecurT) -> bool: # Note: It is correct and significant that the default return value # from calc_is_empty is False despite the default value for is_empty # being true. The reason for this is that strategies should be treated # as empty absent evidence to the contrary, but most basic strategies # are trivially non-empty and it would be annoying to have to override # this method to show that. return False def calc_has_reusable_values(self, recur: RecurT) -> bool: return False def example(self) -> Ex: # FIXME """Provide an example of the sort of value that this strategy generates. This method is designed for use in a REPL, and will raise an error if called from inside |@given| or a strategy definition. For serious use, see |@composite| or |st.data|. """ if getattr(sys, "ps1", None) is None and ( # The main module's __spec__ is None when running interactively # or running a source file directly. # See https://docs.python.org/3/reference/import.html#main-spec. sys.modules["__main__"].__spec__ is not None # __spec__ is also None under pytest-xdist. To avoid an unfortunate # missed alarm here, always warn under pytest. or os.environ.get("PYTEST_CURRENT_TEST") is not None ): # pragma: no branch # The other branch *is* covered in cover/test_interactive_example.py; # but as that uses `pexpect` for an interactive session `coverage` # doesn't see it. warnings.warn( "The `.example()` method is good for exploring strategies, but should " "only be used interactively. We recommend using `@given` for tests - " "it performs better, saves and replays failures to avoid flakiness, " f"and reports minimal examples. (strategy: {self!r})", NonInteractiveExampleWarning, stacklevel=2, ) context = _current_build_context.value if context is not None: if context.data is not None and context.data.depth > 0: raise HypothesisException( "Using example() inside a strategy definition is a bad " "idea. Instead consider using hypothesis.strategies.builds() " "or @hypothesis.strategies.composite to define your strategy." " See https://hypothesis.readthedocs.io/en/latest/reference/" "strategies.html#hypothesis.strategies.builds or " "https://hypothesis.readthedocs.io/en/latest/reference/" "strategies.html#hypothesis.strategies.composite for more " "details." ) else: raise HypothesisException( "Using example() inside a test function is a bad " "idea. Instead consider using hypothesis.strategies.data() " "to draw more examples during testing. See " "https://hypothesis.readthedocs.io/en/latest/reference/" "strategies.html#hypothesis.strategies.data for more details." ) try: return self.__examples.pop() except (AttributeError, IndexError): self.__examples: list[Ex] = [] from hypothesis.core import given # Note: this function has a weird name because it might appear in # tracebacks, and we want users to know that they can ignore it. @given(self) @settings( database=None, # generate only a few examples at a time to avoid slow interactivity # for large strategies. The overhead of @given is very small relative # to generation, so a small batch size is fine. max_examples=10, deadline=None, verbosity=Verbosity.quiet, phases=(Phase.generate,), suppress_health_check=list(HealthCheck), ) def example_generating_inner_function( ex: Ex, # type: ignore # mypy is overzealous in preventing covariant params ) -> None: self.__examples.append(ex) example_generating_inner_function() shuffle(self.__examples) return self.__examples.pop() def map(self, pack: Callable[[Ex], T]) -> "SearchStrategy[T]": """Returns a new strategy which generates a value from this one, and then returns ``pack(value)``. For example, ``integers().map(str)`` could generate ``str(5)`` == ``"5"``. """ if is_identity_function(pack): return self # type: ignore # Mypy has no way to know that `Ex == T` return MappedStrategy(self, pack=pack) def flatmap( self, expand: Callable[[Ex], "SearchStrategy[T]"] ) -> "SearchStrategy[T]": # FIXME """Old syntax for a special case of |@composite|: .. code-block:: python @st.composite def flatmap_like(draw, base_strategy, expand): value = draw(base_strategy) new_strategy = expand(value) return draw(new_strategy) We find that the greater readability of |@composite| usually outweighs the verbosity, with a few exceptions for simple cases or recipes like ``from_type(type).flatmap(from_type)`` ("pick a type, get a strategy for any instance of that type, and then generate one of those"). """ from hypothesis.strategies._internal.flatmapped import FlatMapStrategy return FlatMapStrategy(self, expand=expand) # Note that we previously had condition extracted to a type alias as # PredicateT. However, that was only useful when not specifying a relationship # between the generic Ts and some other function param / return value. # If we do want to - like here, where we want to say that the Ex arg to condition # is of the same type as the strategy's Ex - then you need to write out the # entire Callable[[Ex], Any] expression rather than use a type alias. # TypeAlias is *not* simply a macro that inserts the text. TypeAlias will not # reference the local TypeVar context. def filter(self, condition: Callable[[Ex], Any]) -> "SearchStrategy[Ex]": """Returns a new strategy that generates values from this strategy which satisfy the provided condition. Note that if the condition is too hard to satisfy this might result in your tests failing with an Unsatisfiable exception. A basic version of the filtering logic would look something like: .. code-block:: python @st.composite def filter_like(draw, strategy, condition): for _ in range(3): value = draw(strategy) if condition(value): return value assume(False) """ return FilteredStrategy(self, conditions=(condition,)) @property def branches(self) -> Sequence["SearchStrategy[Ex]"]: return [self] def __or__(self, other: "SearchStrategy[T]") -> "SearchStrategy[Ex | T]": """Return a strategy which produces values by randomly drawing from one of this strategy or the other strategy. This method is part of the public API. """ if not isinstance(other, SearchStrategy): raise ValueError(f"Cannot | a SearchStrategy with {other!r}") # Unwrap explicitly or'd strategies. This turns the # common case of e.g. st.integers() | st.integers() | st.integers() from # # one_of(one_of(integers(), integers()), integers()) # # into # # one_of(integers(), integers(), integers()) # # This is purely an aesthetic unwrapping, for e.g. reprs. In practice # we use .branches / .element_strategies to get the list of possible # strategies, so this unwrapping is *not* necessary for correctness. strategies: list[SearchStrategy] = [] strategies.extend( self.original_strategies if isinstance(self, OneOfStrategy) else [self] ) strategies.extend( other.original_strategies if isinstance(other, OneOfStrategy) else [other] ) return OneOfStrategy(strategies) def __bool__(self) -> bool: warnings.warn( f"bool({self!r}) is always True, did you mean to draw a value?", HypothesisWarning, stacklevel=2, ) return True def validate(self) -> None: """Throw an exception if the strategy is not valid. Strategies should implement ``do_validate``, which is called by this method. They should not override ``validate``. This can happen due to invalid arguments, or lazy construction. """ thread_id = threading.get_ident() if self.validate_called.get(thread_id, False): return # we need to set validate_called before calling do_validate, for # recursive / deferred strategies. But if a thread switches after # validate_called but before do_validate, we might have a strategy # which does weird things like drawing when do_validate would error but # its params are technically valid (e.g. a param was passed as 1.0 # instead of 1) and get into weird internal states. # # There are two ways to fix this. # (1) The first is a per-strategy lock around do_validate. Even though we # expect near-zero lock contention, this still adds the lock overhead. # (2) The second is allowing concurrent .validate calls. Since validation # is (assumed to be) deterministic, both threads will produce the same # end state, so the validation order or race conditions does not matter. # # In order to avoid the lock overhead of (1), we use (2) here. See also # discussion in https://github.com/HypothesisWorks/hypothesis/pull/4473. try: self.validate_called[thread_id] = True self.do_validate() self.is_empty self.has_reusable_values except Exception: self.validate_called[thread_id] = False raise @property def class_label(self) -> int: cls = self.__class__ try: return cls.LABELS[cls] except KeyError: pass result = calc_label_from_cls(cls) cls.LABELS[cls] = result return result @property def label(self) -> int: if isinstance((label := self.__label), int): # avoid locking if we've already completely computed the label. return label with label_lock: if self.__label is calculating: return 0 self.__label = calculating self.__label = self.calc_label() return self.__label def calc_label(self) -> int: return self.class_label def do_validate(self) -> None: pass def do_draw(self, data: ConjectureData) -> Ex: raise NotImplementedError(f"{type(self).__name__}.do_draw") def _is_hashable(value: object) -> tuple[bool, int | None]: # hashing can be expensive; return the hash value if we compute it, so that # callers don't have to recompute. try: return (True, hash(value)) except TypeError: return (False, None) def is_hashable(value: object) -> bool: return _is_hashable(value)[0] class SampledFromStrategy(SearchStrategy[Ex]): """A strategy which samples from a set of elements. This is essentially equivalent to using a OneOfStrategy over Just strategies but may be more efficient and convenient. """ _MAX_FILTER_CALLS: ClassVar[int] = 10_000 def __init__( self, elements: Sequence[Ex], *, force_repr: str | None = None, force_repr_braces: tuple[str, str] | None = None, transformations: tuple[ tuple[Literal["filter", "map"], Callable[[Ex], Any]], ..., ] = (), ): super().__init__() self.elements = cu.check_sample(elements, "sampled_from") assert self.elements self.force_repr = force_repr self.force_repr_braces = force_repr_braces self._transformations = transformations self._cached_repr: str | None = None def map(self, pack: Callable[[Ex], T]) -> SearchStrategy[T]: s = type(self)( self.elements, force_repr=self.force_repr, force_repr_braces=self.force_repr_braces, transformations=(*self._transformations, ("map", pack)), ) # guaranteed by the ("map", pack) transformation return cast(SearchStrategy[T], s) def filter(self, condition: Callable[[Ex], Any]) -> SearchStrategy[Ex]: return type(self)( self.elements, force_repr=self.force_repr, force_repr_braces=self.force_repr_braces, transformations=(*self._transformations, ("filter", condition)), ) def __repr__(self): if self._cached_repr is None: rep = get_pretty_function_description elements_s = ( ", ".join(rep(v) for v in self.elements[:512]) + ", ..." if len(self.elements) > 512 else ", ".join(rep(v) for v in self.elements) ) braces = self.force_repr_braces or ("(", ")") instance_s = ( self.force_repr or f"sampled_from({braces[0]}{elements_s}{braces[1]})" ) transforms_s = "".join( f".{name}({get_pretty_function_description(f)})" for name, f in self._transformations ) repr_s = instance_s + transforms_s self._cached_repr = repr_s return self._cached_repr def calc_label(self) -> int: # strategy.label is effectively an under-approximation of structural # equality (i.e., some strategies may have the same label when they are not # structurally identical). More importantly for calculating the # SampledFromStrategy label, we might have hash(s1) != hash(s2) even # when s1 and s2 are structurally identical. For instance: # # s1 = st.sampled_from([st.none()]) # s2 = st.sampled_from([st.none()]) # assert hash(s1) != hash(s2) # # (see also test cases in test_labels.py). # # We therefore use the labels of any component strategies when calculating # our label, and only use the hash if it is not a strategy. # # That's the ideal, anyway. In reality the logic is more complicated than # necessary in order to be efficient in the presence of (very) large sequences: # * add an unabashed special case for range, to avoid iteration over an # enormous range when we know it is entirely integers. # * if there is at least one strategy in self.elements, use strategy label, # and the element hash otherwise. # * if there are no strategies in self.elements, take the hash of the # entire sequence. This prevents worst-case performance of hashing each # element when a hash of the entire sequence would have sufficed. # # The worst case performance of this scheme is # itertools.chain(range(2**100), [st.none()]), where it degrades to # hashing every int in the range. (elements_is_hashable, hash_value) = _is_hashable(self.elements) if isinstance(self.elements, range) or ( elements_is_hashable and not any(isinstance(e, SearchStrategy) for e in self.elements) ): return combine_labels( self.class_label, calc_label_from_name(str(hash_value)) ) labels = [self.class_label] for element in self.elements: if not is_hashable(element): continue labels.append( element.label if isinstance(element, SearchStrategy) else calc_label_from_hash(element) ) return combine_labels(*labels) def calc_has_reusable_values(self, recur: RecurT) -> bool: # Because our custom .map/.filter implementations skip the normal # wrapper strategies (which would automatically return False for us), # we need to manually return False here if any transformations have # been applied. return not self._transformations def calc_is_cacheable(self, recur: RecurT) -> bool: return is_hashable(self.elements) def _transform( self, # https://github.com/python/mypy/issues/7049, we're not writing `element` # anywhere in the class so this is still type-safe. mypy is being more # conservative than necessary element: Ex, # type: ignore ) -> Ex | UniqueIdentifier: # Used in UniqueSampledListStrategy for name, f in self._transformations: if name == "map": result = f(element) if build_context := _current_build_context.value: build_context.record_call(result, f, args=[element], kwargs={}) element = result else: assert name == "filter" if not f(element): return filter_not_satisfied return element def do_draw(self, data: ConjectureData) -> Ex: result = self.do_filtered_draw(data) if isinstance(result, SearchStrategy) and all( isinstance(x, SearchStrategy) for x in self.elements ): data._sampled_from_all_strategies_elements_message = ( "sampled_from was given a collection of strategies: " "{!r}. Was one_of intended?", self.elements, ) if result is filter_not_satisfied: data.mark_invalid(f"Aborted test because unable to satisfy {self!r}") assert not isinstance(result, UniqueIdentifier) return result def get_element(self, i: int) -> Ex | UniqueIdentifier: return self._transform(self.elements[i]) def do_filtered_draw(self, data: ConjectureData) -> Ex | UniqueIdentifier: # Set of indices that have been tried so far, so that we never test # the same element twice during a draw. known_bad_indices: set[int] = set() # Start with ordinary rejection sampling. It's fast if it works, and # if it doesn't work then it was only a small amount of overhead. for _ in range(3): i = data.draw_integer(0, len(self.elements) - 1) if i not in known_bad_indices: element = self.get_element(i) if element is not filter_not_satisfied: return element if not known_bad_indices: data.events[f"Retried draw from {self!r} to satisfy filter"] = "" known_bad_indices.add(i) # If we've tried all the possible elements, give up now. max_good_indices = len(self.elements) - len(known_bad_indices) if not max_good_indices: return filter_not_satisfied # Impose an arbitrary cutoff to prevent us from wasting too much time # on very large element lists. max_good_indices = min(max_good_indices, self._MAX_FILTER_CALLS - 3) # Before building the list of allowed indices, speculatively choose # one of them. We don't yet know how many allowed indices there will be, # so this choice might be out-of-bounds, but that's OK. speculative_index = data.draw_integer(0, max_good_indices - 1) # Calculate the indices of allowed values, so that we can choose one # of them at random. But if we encounter the speculatively-chosen one, # just use that and return immediately. Note that we also track the # allowed elements, in case of .map(some_stateful_function) allowed: list[tuple[int, Ex]] = [] for i in range(min(len(self.elements), self._MAX_FILTER_CALLS - 3)): if i not in known_bad_indices: element = self.get_element(i) if element is not filter_not_satisfied: assert not isinstance(element, UniqueIdentifier) allowed.append((i, element)) if len(allowed) > speculative_index: # Early-exit case: We reached the speculative index, so # we just return the corresponding element. data.draw_integer(0, len(self.elements) - 1, forced=i) return element # The speculative index didn't work out, but at this point we've built # and can choose from the complete list of allowed indices and elements. if allowed: i, element = data.choice(allowed) data.draw_integer(0, len(self.elements) - 1, forced=i) return element # If there are no allowed indices, the filter couldn't be satisfied. return filter_not_satisfied class OneOfStrategy(SearchStrategy[Ex]): """Implements a union of strategies. Given a number of strategies this generates values which could have come from any of them. The conditional distribution draws uniformly at random from some non-empty subset of these strategies and then draws from the conditional distribution of that strategy. """ def __init__(self, strategies: Sequence[SearchStrategy[Ex]]): super().__init__() self.original_strategies = tuple(strategies) self.__element_strategies: Sequence[SearchStrategy[Ex]] | None = None self.__in_branches = False self._branches_lock = RLock() def calc_is_empty(self, recur: RecurT) -> bool: return all(recur(e) for e in self.original_strategies) def calc_has_reusable_values(self, recur: RecurT) -> bool: return all(recur(e) for e in self.original_strategies) def calc_is_cacheable(self, recur: RecurT) -> bool: return all(recur(e) for e in self.original_strategies) @property def element_strategies(self) -> Sequence[SearchStrategy[Ex]]: if self.__element_strategies is None: # While strategies are hashable, they use object.__hash__ and are # therefore distinguished only by identity. # # In principle we could "just" define a __hash__ method # (and __eq__, but that's easy in terms of type() and hash()) # to make this more powerful, but this is harder than it sounds: # # 1. Strategies are often distinguished by non-hashable attributes, # or by attributes that have the same hash value ("^.+" / b"^.+"). # 2. LazyStrategy: can't reify the wrapped strategy without breaking # laziness, so there's a hash each for the lazy and the nonlazy. # # Having made several attempts, the minor benefits of making strategies # hashable are simply not worth the engineering effort it would take. # See also issues #2291 and #2327. seen: set[SearchStrategy] = {self} strategies: list[SearchStrategy] = [] for arg in self.original_strategies: check_strategy(arg) if not arg.is_empty: for s in arg.branches: if s not in seen and not s.is_empty: seen.add(s) strategies.append(s) self.__element_strategies = strategies return self.__element_strategies def calc_label(self) -> int: return combine_labels( self.class_label, *(p.label for p in self.original_strategies) ) def do_draw(self, data: ConjectureData) -> Ex: strategy = data.draw( SampledFromStrategy(self.element_strategies).filter( lambda s: not s.is_currently_empty(data) ) ) return data.draw(strategy) def __repr__(self) -> str: return "one_of({})".format(", ".join(map(repr, self.original_strategies))) def do_validate(self) -> None: for e in self.element_strategies: e.validate() @property def branches(self) -> Sequence[SearchStrategy[Ex]]: if self.__element_strategies is not None: # common fast path which avoids the lock return self.element_strategies with self._branches_lock: if not self.__in_branches: try: self.__in_branches = True return self.element_strategies finally: self.__in_branches = False else: return [self] def filter(self, condition: Callable[[Ex], Any]) -> SearchStrategy[Ex]: return FilteredStrategy( OneOfStrategy([s.filter(condition) for s in self.original_strategies]), conditions=(), ) @overload def one_of( __args: Sequence[SearchStrategy[Ex]], ) -> SearchStrategy[Ex]: # pragma: no cover ... @overload def one_of(__a1: SearchStrategy[Ex]) -> SearchStrategy[Ex]: # pragma: no cover ... @overload def one_of( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T] ) -> SearchStrategy[Ex | T]: # pragma: no cover ... @overload def one_of( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3] ) -> SearchStrategy[Ex | T | T3]: # pragma: no cover ... @overload def one_of( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3], __a4: SearchStrategy[T4], ) -> SearchStrategy[Ex | T | T3 | T4]: # pragma: no cover ... @overload def one_of( __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3], __a4: SearchStrategy[T4], __a5: SearchStrategy[T5], ) -> SearchStrategy[Ex | T | T3 | T4 | T5]: # pragma: no cover ... @overload def one_of(*args: SearchStrategy[Any]) -> SearchStrategy[Any]: # pragma: no cover ... @defines_strategy(eager=True) def one_of( *args: Sequence[SearchStrategy[Any]] | SearchStrategy[Any], ) -> SearchStrategy[Any]: # Mypy workaround alert: Any is too loose above; the return parameter # should be the union of the input parameters. Unfortunately, Mypy <=0.600 # raises errors due to incompatible inputs instead. See #1270 for links. # v0.610 doesn't error; it gets inference wrong for 2+ arguments instead. """Return a strategy which generates values from any of the argument strategies. This may be called with one iterable argument instead of multiple strategy arguments, in which case ``one_of(x)`` and ``one_of(*x)`` are equivalent. Examples from this strategy will generally shrink to ones that come from strategies earlier in the list, then shrink according to behaviour of the strategy that produced them. In order to get good shrinking behaviour, try to put simpler strategies first. e.g. ``one_of(none(), text())`` is better than ``one_of(text(), none())``. This is especially important when using recursive strategies. e.g. ``x = st.deferred(lambda: st.none() | st.tuples(x, x))`` will shrink well, but ``x = st.deferred(lambda: st.tuples(x, x) | st.none())`` will shrink very badly indeed. """ if len(args) == 1 and not isinstance(args[0], SearchStrategy): try: args = tuple(args[0]) except TypeError: pass if len(args) == 1 and isinstance(args[0], SearchStrategy): # This special-case means that we can one_of over lists of any size # without incurring any performance overhead when there is only one # strategy, and keeps our reprs simple. return args[0] if args and not any(isinstance(a, SearchStrategy) for a in args): # And this special case is to give a more-specific error message if it # seems that the user has confused `one_of()` for `sampled_from()`; # the remaining validation is left to OneOfStrategy. See PR #2627. raise InvalidArgument( f"Did you mean st.sampled_from({list(args)!r})? st.one_of() is used " "to combine strategies, but all of the arguments were of other types." ) # we've handled the case where args is a one-element sequence [(s1, s2, ...)] # above, so we can assume it's an actual sequence of strategies. args = cast(Sequence[SearchStrategy], args) return OneOfStrategy(args) class MappedStrategy(SearchStrategy[MappedTo], Generic[MappedFrom, MappedTo]): """A strategy which is defined purely by conversion to and from another strategy. Its parameter and distribution come from that other strategy. """ def __init__( self, strategy: SearchStrategy[MappedFrom], pack: Callable[[MappedFrom], MappedTo], ) -> None: super().__init__() self.mapped_strategy = strategy self.pack = pack def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.mapped_strategy) def calc_is_cacheable(self, recur: RecurT) -> bool: return recur(self.mapped_strategy) def __repr__(self) -> str: if not hasattr(self, "_cached_repr"): self._cached_repr = f"{self.mapped_strategy!r}.map({get_pretty_function_description(self.pack)})" return self._cached_repr def do_validate(self) -> None: self.mapped_strategy.validate() def do_draw(self, data: ConjectureData) -> MappedTo: with warnings.catch_warnings(): if isinstance(self.pack, type) and issubclass( self.pack, (abc.Mapping, abc.Set) ): warnings.simplefilter("ignore", BytesWarning) for _ in range(3): try: data.start_span(MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL) x = data.draw(self.mapped_strategy) result = self.pack(x) data.stop_span() current_build_context().record_call( result, self.pack, args=[x], kwargs={} ) return result except UnsatisfiedAssumption: data.stop_span(discard=True) raise UnsatisfiedAssumption @property def branches(self) -> Sequence[SearchStrategy[MappedTo]]: return [ MappedStrategy(strategy, pack=self.pack) for strategy in self.mapped_strategy.branches ] def filter( self, condition: Callable[[MappedTo], Any] ) -> "SearchStrategy[MappedTo]": # Includes a special case so that we can rewrite filters on collection # lengths, when most collections are `st.lists(...).map(the_type)`. ListStrategy = _list_strategy_type() if not isinstance(self.mapped_strategy, ListStrategy) or not ( (isinstance(self.pack, type) and issubclass(self.pack, abc.Collection)) or self.pack in _collection_ish_functions() ): return super().filter(condition) # Check whether our inner list strategy can rewrite this filter condition. # If not, discard the result and _only_ apply a new outer filter. new = ListStrategy.filter(self.mapped_strategy, condition) if getattr(new, "filtered_strategy", None) is self.mapped_strategy: return super().filter(condition) # didn't rewrite # Apply a new outer filter even though we rewrote the inner strategy, # because some collections can change the list length (dict, set, etc). return FilteredStrategy(type(self)(new, self.pack), conditions=(condition,)) @lru_cache def _list_strategy_type() -> Any: from hypothesis.strategies._internal.collections import ListStrategy return ListStrategy def _collection_ish_functions() -> Sequence[Any]: funcs = [sorted] if np := sys.modules.get("numpy"): # c.f. https://numpy.org/doc/stable/reference/routines.array-creation.html # Probably only `np.array` and `np.asarray` will be used in practice, # but why should that stop us when we've already gone this far? funcs += [ np.empty_like, np.eye, np.identity, np.ones_like, np.zeros_like, np.array, np.asarray, np.asanyarray, np.ascontiguousarray, np.asmatrix, np.copy, np.rec.array, np.rec.fromarrays, np.rec.fromrecords, np.diag, # bonus undocumented functions from tab-completion: np.asarray_chkfinite, np.asfortranarray, ] return funcs filter_not_satisfied = UniqueIdentifier("filter not satisfied") class FilteredStrategy(SearchStrategy[Ex]): def __init__( self, strategy: SearchStrategy[Ex], conditions: tuple[Callable[[Ex], Any], ...] ): super().__init__() if isinstance(strategy, FilteredStrategy): # Flatten chained filters into a single filter with multiple conditions. self.flat_conditions: tuple[Callable[[Ex], Any], ...] = ( strategy.flat_conditions + conditions ) self.filtered_strategy: SearchStrategy[Ex] = strategy.filtered_strategy else: self.flat_conditions = conditions self.filtered_strategy = strategy assert isinstance(self.flat_conditions, tuple) assert not isinstance(self.filtered_strategy, FilteredStrategy) self.__condition: Callable[[Ex], Any] | None = None def calc_is_empty(self, recur: RecurT) -> bool: return recur(self.filtered_strategy) def calc_is_cacheable(self, recur: RecurT) -> bool: return recur(self.filtered_strategy) def __repr__(self) -> str: if not hasattr(self, "_cached_repr"): self._cached_repr = "{!r}{}".format( self.filtered_strategy, "".join( f".filter({get_pretty_function_description(cond)})" for cond in self.flat_conditions ), ) return self._cached_repr def do_validate(self) -> None: # Start by validating our inner filtered_strategy. If this was a LazyStrategy, # validation also reifies it so that subsequent calls to e.g. `.filter()` will # be passed through. self.filtered_strategy.validate() # So now we have a reified inner strategy, we'll replay all our saved # predicates in case some or all of them can be rewritten. Note that this # replaces the `fresh` strategy too! fresh = self.filtered_strategy for cond in self.flat_conditions: fresh = fresh.filter(cond) if isinstance(fresh, FilteredStrategy): # In this case we have at least some non-rewritten filter predicates, # so we just re-initialize the strategy. FilteredStrategy.__init__( self, fresh.filtered_strategy, fresh.flat_conditions ) else: # But if *all* the predicates were rewritten... well, do_validate() is # an in-place method so we still just re-initialize the strategy! FilteredStrategy.__init__(self, fresh, ()) def filter(self, condition: Callable[[Ex], Any]) -> "FilteredStrategy[Ex]": # If we can, it's more efficient to rewrite our strategy to satisfy the # condition. We therefore exploit the fact that the order of predicates # doesn't matter (`f(x) and g(x) == g(x) and f(x)`) by attempting to apply # condition directly to our filtered strategy as the inner-most filter. out = self.filtered_strategy.filter(condition) # If it couldn't be rewritten, we'll get a new FilteredStrategy - and then # combine the conditions of each in our expected newest=last order. if isinstance(out, FilteredStrategy): return FilteredStrategy( out.filtered_strategy, self.flat_conditions + out.flat_conditions ) # But if it *could* be rewritten, we can return the more efficient form! return FilteredStrategy(out, self.flat_conditions) @property def condition(self) -> Callable[[Ex], Any]: # We write this defensively to avoid any threading race conditions # with our manual FilteredStrategy.__init__ for filter-rewriting. # See https://github.com/HypothesisWorks/hypothesis/pull/4522. if (condition := self.__condition) is not None: return condition if len(self.flat_conditions) == 1: # Avoid an extra indirection in the common case of only one condition. condition = self.flat_conditions[0] elif len(self.flat_conditions) == 0: # Possible, if unlikely, due to filter predicate rewriting condition = lambda _: True # type: ignore # covariant type param else: condition = lambda x: all( # type: ignore # covariant type param cond(x) for cond in self.flat_conditions ) self.__condition = condition return condition def do_draw(self, data: ConjectureData) -> Ex: result = self.do_filtered_draw(data) if result is not filter_not_satisfied: return cast(Ex, result) data.mark_invalid(f"Aborted test because unable to satisfy {self!r}") def do_filtered_draw(self, data: ConjectureData) -> Ex | UniqueIdentifier: for i in range(3): data.start_span(FILTERED_SEARCH_STRATEGY_DO_DRAW_LABEL) value = data.draw(self.filtered_strategy) if self.condition(value): data.stop_span() return value else: data.stop_span(discard=True) if i == 0: data.events[f"Retried draw from {self!r} to satisfy filter"] = "" return filter_not_satisfied @property def branches(self) -> Sequence[SearchStrategy[Ex]]: return [ FilteredStrategy(strategy=strategy, conditions=self.flat_conditions) for strategy in self.filtered_strategy.branches ] @check_function def check_strategy(arg: object, name: str = "") -> None: assert isinstance(name, str) if not isinstance(arg, SearchStrategy): hint = "" if isinstance(arg, (list, tuple)): hint = ", such as st.sampled_from({}),".format(name or "...") if name: name += "=" raise InvalidArgument( f"Expected a SearchStrategy{hint} but got {name}{arg!r} " f"(type={type(arg).__name__})" ) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/strings.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import copy import re import warnings from collections.abc import Collection from functools import cache, lru_cache, partial from typing import cast from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.internal import charmap from hypothesis.internal.charmap import Categories from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.providers import COLLECTION_DEFAULT_MAX_SIZE from hypothesis.internal.filtering import max_len, min_len from hypothesis.internal.intervalsets import IntervalSet from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.collections import ListStrategy from hypothesis.strategies._internal.lazy import unwrap_strategies from hypothesis.strategies._internal.strategies import ( OneOfStrategy, SampledFromStrategy, SearchStrategy, ) from hypothesis.vendor.pretty import pretty # Cache size is limited by sys.maxunicode, but passing None makes it slightly faster. @cache # this is part of our forward-facing validation, so we do *not* tell mypyc that c # should be a str, because we don't want it to validate it before we can. def _check_is_single_character(c: object) -> str: # In order to mitigate the performance cost of this check, we use a shared cache, # even at the cost of showing the culprit strategy in the error message. if not isinstance(c, str): type_ = get_pretty_function_description(type(c)) raise InvalidArgument(f"Got non-string {c!r} (type {type_})") if len(c) != 1: raise InvalidArgument(f"Got {c!r} (length {len(c)} != 1)") return c class OneCharStringStrategy(SearchStrategy[str]): """A strategy which generates single character strings of text type.""" def __init__(self, intervals: IntervalSet, force_repr: str | None = None) -> None: super().__init__() assert isinstance(intervals, IntervalSet) self.intervals = intervals self._force_repr = force_repr @classmethod def from_characters_args( cls, *, codec: str | None = None, min_codepoint: int | None = None, max_codepoint: int | None = None, categories: Categories | None = None, exclude_characters: Collection[str] = "", include_characters: Collection[str] = "", ) -> "OneCharStringStrategy": assert set(categories or ()).issubset(charmap.categories()) intervals = charmap.query( min_codepoint=min_codepoint, max_codepoint=max_codepoint, categories=categories, exclude_characters=exclude_characters, include_characters=include_characters, ) if codec is not None: intervals &= charmap.intervals_from_codec(codec) _arg_repr = ", ".join( f"{k}={v!r}" for k, v in [ ("codec", codec), ("min_codepoint", min_codepoint), ("max_codepoint", max_codepoint), ("categories", categories), ("exclude_characters", exclude_characters), ("include_characters", include_characters), ] if v not in (None, "") and not ( k == "categories" # v has to be `categories` here. Help mypy along to infer that. and set(cast(Categories, v)) == set(charmap.categories()) - {"Cs"} ) ) if not intervals: raise InvalidArgument( "No characters are allowed to be generated by this " f"combination of arguments: {_arg_repr}" ) return cls(intervals, force_repr=f"characters({_arg_repr})") @classmethod def from_alphabet(cls, alphabet: str | SearchStrategy) -> "OneCharStringStrategy": if isinstance(alphabet, str): return cls.from_characters_args(categories=(), include_characters=alphabet) assert isinstance(alphabet, SearchStrategy) char_strategy = unwrap_strategies(alphabet) if isinstance(char_strategy, cls): return char_strategy elif isinstance(char_strategy, SampledFromStrategy): for c in char_strategy.elements: _check_is_single_character(c) return cls.from_characters_args( categories=(), include_characters=char_strategy.elements, ) elif isinstance(char_strategy, OneOfStrategy): intervals = IntervalSet() for s in char_strategy.element_strategies: intervals = intervals.union(cls.from_alphabet(s).intervals) return cls(intervals, force_repr=repr(alphabet)) raise InvalidArgument( f"{alphabet=} must be a sampled_from() or characters() strategy" ) def __repr__(self) -> str: return self._force_repr or f"OneCharStringStrategy({self.intervals!r})" def do_draw(self, data: ConjectureData) -> str: return data.draw_string(self.intervals, min_size=1, max_size=1) _nonempty_names = ( "capitalize", "expandtabs", "join", "lower", "rsplit", "split", "splitlines", "swapcase", "title", "upper", ) _nonempty_and_content_names = ( "islower", "isupper", "isalnum", "isalpha", "isascii", "isdigit", "isspace", "istitle", "lstrip", "rstrip", "strip", ) class TextStrategy(ListStrategy[str]): def do_draw(self, data): # if our element strategy is OneCharStringStrategy, we can skip the # ListStrategy draw and jump right to data.draw_string. # Doing so for user-provided element strategies is not correct in # general, as they may define a different distribution than data.draw_string. elems = unwrap_strategies(self.element_strategy) if isinstance(elems, OneCharStringStrategy): return data.draw_string( elems.intervals, min_size=self.min_size, max_size=( COLLECTION_DEFAULT_MAX_SIZE if self.max_size == float("inf") else self.max_size ), ) return "".join(super().do_draw(data)) def __repr__(self) -> str: args = [] if repr(self.element_strategy) != "characters()": args.append(repr(self.element_strategy)) if self.min_size: args.append(f"min_size={self.min_size}") if self.max_size < float("inf"): args.append(f"max_size={self.max_size}") return f"text({', '.join(args)})" # See https://docs.python.org/3/library/stdtypes.html#string-methods # These methods always return Truthy values for any nonempty string. _nonempty_filters = ( *ListStrategy._nonempty_filters, str, str.casefold, str.encode, *(getattr(str, n) for n in _nonempty_names), ) _nonempty_and_content_filters = ( str.isdecimal, str.isnumeric, *(getattr(str, n) for n in _nonempty_and_content_names), ) def filter(self, condition): elems = unwrap_strategies(self.element_strategy) if ( condition is str.isidentifier and self.max_size >= 1 and isinstance(elems, OneCharStringStrategy) ): from hypothesis.strategies import builds, nothing id_start, id_continue = _identifier_characters() if not (elems.intervals & id_start): return nothing() return builds( "{}{}".format, OneCharStringStrategy(elems.intervals & id_start), TextStrategy( OneCharStringStrategy(elems.intervals & id_continue), min_size=max(0, self.min_size - 1), max_size=self.max_size - 1, ), # Filter to ensure that NFKC normalization keeps working in future ).filter(str.isidentifier) if (new := _string_filter_rewrite(self, str, condition)) is not None: return new return super().filter(condition) def _string_filter_rewrite(self, kind, condition): if condition in (kind.lower, kind.title, kind.upper): k = kind.__name__ warnings.warn( f"You applied {k}.{condition.__name__} as a filter, but this allows " f"all nonempty strings! Did you mean {k}.is{condition.__name__}?", HypothesisWarning, stacklevel=2, ) if ( ( kind is bytes or isinstance( unwrap_strategies(self.element_strategy), OneCharStringStrategy ) ) and isinstance(pattern := getattr(condition, "__self__", None), re.Pattern) and isinstance(pattern.pattern, kind) ): from hypothesis.strategies._internal.regex import regex_strategy if condition.__name__ == "match": # Replace with an easier-to-handle equivalent condition caret, close = ("^(?:", ")") if kind is str else (b"^(?:", b")") pattern = re.compile(caret + pattern.pattern + close, flags=pattern.flags) condition = pattern.search if condition.__name__ in ("search", "findall", "fullmatch"): s = regex_strategy( pattern, fullmatch=condition.__name__ == "fullmatch", alphabet=self.element_strategy if kind is str else None, ) if self.min_size > 0: s = s.filter(partial(min_len, self.min_size)) if self.max_size < 1e999: s = s.filter(partial(max_len, self.max_size)) return s elif condition.__name__ in ("finditer", "scanner"): # PyPy implements `finditer` as an alias to their `scanner` method warnings.warn( f"You applied {pretty(condition)} as a filter, but this allows " f"any string at all! Did you mean .findall ?", HypothesisWarning, stacklevel=3, ) return self elif condition.__name__ == "split": warnings.warn( f"You applied {pretty(condition)} as a filter, but this allows " f"any nonempty string! Did you mean .search ?", HypothesisWarning, stacklevel=3, ) return self.filter(bool) # We use ListStrategy filter logic for the conditions that *only* imply # the string is nonempty. Here, we increment the min_size but still apply # the filter for conditions that imply nonempty *and specific contents*. if condition in self._nonempty_and_content_filters and self.max_size >= 1: self = copy.copy(self) self.min_size = max(1, self.min_size) return ListStrategy.filter(self, condition) return None # Excerpted from https://www.unicode.org/Public/15.0.0/ucd/PropList.txt # Python updates it's Unicode version between minor releases, but fortunately # these properties do not change between the Unicode versions in question. _PROPLIST = """ # ================================================ 1885..1886 ; Other_ID_Start # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA 2118 ; Other_ID_Start # Sm SCRIPT CAPITAL P 212E ; Other_ID_Start # So ESTIMATED SYMBOL 309B..309C ; Other_ID_Start # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK # Total code points: 6 # ================================================ 00B7 ; Other_ID_Continue # Po MIDDLE DOT 0387 ; Other_ID_Continue # Po GREEK ANO TELEIA 1369..1371 ; Other_ID_Continue # No [9] ETHIOPIC DIGIT ONE..ETHIOPIC DIGIT NINE 19DA ; Other_ID_Continue # No NEW TAI LUE THAM DIGIT ONE # Total code points: 12 """ @lru_cache def _identifier_characters() -> tuple[IntervalSet, IntervalSet]: """See https://docs.python.org/3/reference/lexical_analysis.html#identifiers""" # Start by computing the set of special characters chars = {"Other_ID_Start": "", "Other_ID_Continue": ""} for line in _PROPLIST.splitlines(): if m := re.match(r"([0-9A-F.]+) +; (\w+) # ", line): codes, prop = m.groups() span = range(int(codes[:4], base=16), int(codes[-4:], base=16) + 1) chars[prop] += "".join(chr(x) for x in span) # Then get the basic set by Unicode category and known extras id_start = charmap.query( categories=("Lu", "Ll", "Lt", "Lm", "Lo", "Nl"), include_characters="_" + chars["Other_ID_Start"], ) id_start -= IntervalSet.from_string( # Magic value: the characters which NFKC-normalize to be invalid identifiers. # Conveniently they're all in `id_start`, so we only need to do this once. "\u037a\u0e33\u0eb3\u2e2f\u309b\u309c\ufc5e\ufc5f\ufc60\ufc61\ufc62\ufc63" "\ufdfa\ufdfb\ufe70\ufe72\ufe74\ufe76\ufe78\ufe7a\ufe7c\ufe7e\uff9e\uff9f" ) id_continue = id_start | charmap.query( categories=("Mn", "Mc", "Nd", "Pc"), include_characters=chars["Other_ID_Continue"], ) return id_start, id_continue class BytesStrategy(SearchStrategy): def __init__(self, min_size: int, max_size: int | None): super().__init__() self.min_size = min_size self.max_size = ( max_size if max_size is not None else COLLECTION_DEFAULT_MAX_SIZE ) def do_draw(self, data: ConjectureData) -> bytes: return data.draw_bytes(self.min_size, self.max_size) _nonempty_filters = ( *ListStrategy._nonempty_filters, bytes, *(getattr(bytes, n) for n in _nonempty_names), ) _nonempty_and_content_filters = ( *(getattr(bytes, n) for n in _nonempty_and_content_names), ) def filter(self, condition): if (new := _string_filter_rewrite(self, bytes, condition)) is not None: return new return ListStrategy.filter(self, condition) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/types.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import builtins import collections import collections.abc import datetime import decimal import fractions import functools import inspect import io import ipaddress import numbers import operator import os import random import re import sys import typing import uuid import warnings import zoneinfo from collections.abc import Iterator from functools import partial from pathlib import PurePath from types import FunctionType from typing import TYPE_CHECKING, Any, NewType, get_args, get_origin from hypothesis import strategies as st from hypothesis.errors import ( HypothesisException, HypothesisWarning, InvalidArgument, ResolutionFailed, ) from hypothesis.internal.compat import PYPY, BaseExceptionGroup, ExceptionGroup from hypothesis.internal.conjecture.utils import many as conjecture_utils_many from hypothesis.internal.filtering import max_len, min_len from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.ipaddress import ( SPECIAL_IPv4_RANGES, SPECIAL_IPv6_RANGES, ip_addresses, ) from hypothesis.strategies._internal.lazy import unwrap_strategies from hypothesis.strategies._internal.strategies import OneOfStrategy if TYPE_CHECKING: import annotated_types as at GenericAlias: typing.Any UnionType: typing.Any try: # The type of PEP-604 unions (`int | str`), added in Python 3.10 from types import GenericAlias, UnionType except ImportError: GenericAlias = () UnionType = () try: import typing_extensions except ImportError: typing_extensions = None # type: ignore try: from typing import _AnnotatedAlias # type: ignore except ImportError: try: from typing_extensions import _AnnotatedAlias except ImportError: _AnnotatedAlias = () ConcatenateTypes: tuple = () try: ConcatenateTypes += (typing.Concatenate,) except AttributeError: # pragma: no cover pass # Is missing for `python<3.10` try: ConcatenateTypes += (typing_extensions.Concatenate,) except AttributeError: # pragma: no cover pass # `typing_extensions` might not be installed ParamSpecTypes: tuple = () try: ParamSpecTypes += (typing.ParamSpec,) except AttributeError: # pragma: no cover pass # Is missing for `python<3.10` try: ParamSpecTypes += (typing_extensions.ParamSpec,) except AttributeError: # pragma: no cover pass # `typing_extensions` might not be installed TypeGuardTypes: tuple = () try: TypeGuardTypes += (typing.TypeGuard,) except AttributeError: # pragma: no cover pass # Is missing for `python<3.10` try: TypeGuardTypes += (typing.TypeIs,) except AttributeError: # pragma: no cover pass # Is missing for `python<3.13` try: TypeGuardTypes += (typing_extensions.TypeGuard, typing_extensions.TypeIs) except AttributeError: # pragma: no cover pass # `typing_extensions` might not be installed RequiredTypes: tuple = () try: RequiredTypes += (typing.Required,) except AttributeError: # pragma: no cover pass # Is missing for `python<3.11` try: RequiredTypes += (typing_extensions.Required,) except AttributeError: # pragma: no cover pass # `typing_extensions` might not be installed NotRequiredTypes: tuple = () try: NotRequiredTypes += (typing.NotRequired,) except AttributeError: # pragma: no cover pass # Is missing for `python<3.11` try: NotRequiredTypes += (typing_extensions.NotRequired,) except AttributeError: # pragma: no cover pass # `typing_extensions` might not be installed ReadOnlyTypes: tuple = () try: ReadOnlyTypes += (typing.ReadOnly,) except AttributeError: # pragma: no cover pass # Is missing for `python<3.13` try: ReadOnlyTypes += (typing_extensions.ReadOnly,) except AttributeError: # pragma: no cover pass # `typing_extensions` might not be installed LiteralStringTypes: tuple = () try: LiteralStringTypes += (typing.LiteralString,) except AttributeError: # pragma: no cover pass # Is missing for `python<3.11` try: LiteralStringTypes += (typing_extensions.LiteralString,) except AttributeError: # pragma: no cover pass # `typing_extensions` might not be installed # We need this function to use `get_origin` on 3.8 for types added later: # in typing-extensions, so we prefer this function over regular `get_origin` # when unwrapping `TypedDict`'s annotations. try: extended_get_origin = typing_extensions.get_origin except AttributeError: # pragma: no cover # `typing_extensions` might not be installed, in this case - fallback: extended_get_origin = get_origin # type: ignore # Used on `TypeVar` objects with no default: NoDefaults = ( getattr(typing, "NoDefault", object()), getattr(typing_extensions, "NoDefault", object()), ) # We use this variable to be sure that we are working with a type from `typing`: typing_root_type = (typing._Final, typing._GenericAlias) # type: ignore # We use this to disallow all non-runtime types from being registered and resolved. # By "non-runtime" we mean: types that do not really exist in python's # and are just added for more fancy type annotations. # `Final` is a great example: it just indicates that this value can't be reassigned. NON_RUNTIME_TYPES = ( typing.Any, typing.Annotated, *ConcatenateTypes, *ParamSpecTypes, *TypeGuardTypes, ) for name in ( "ClassVar", "Final", "NoReturn", "Self", "Required", "NotRequired", "ReadOnly", "Never", "TypeAlias", "TypeVarTuple", "Unpack", ): try: NON_RUNTIME_TYPES += (getattr(typing, name),) except AttributeError: # pragma: no cover pass try: NON_RUNTIME_TYPES += (getattr(typing_extensions, name),) except AttributeError: # pragma: no cover pass # typing_extensions might not be installed def type_sorting_key(t): """Minimise to None, then non-container types, then container types.""" if t is None or t is type(None): return (-1, repr(t)) t = get_origin(t) or t is_container = int(try_issubclass(t, collections.abc.Container)) return (is_container, repr(t)) def _compatible_args(args, superclass_args): """Check that the args of two generic types are compatible for try_issubclass.""" assert superclass_args is not None if args is None: return True return len(args) == len(superclass_args) and all( # "a==b or either is a typevar" is a hacky approximation, but it's # good enough for all the cases that I've seen so far and has the # substantial virtue of (relative) simplicity. a == b or isinstance(a, typing.TypeVar) or isinstance(b, typing.TypeVar) for a, b in zip(args, superclass_args, strict=True) ) def try_issubclass(thing, superclass): try: # In this case we're looking at two distinct classes - which might be generics. # That brings in some complications: if issubclass(get_origin(thing) or thing, get_origin(superclass) or superclass): superclass_args = get_args(superclass) if not superclass_args: # The superclass is not generic, so we're definitely a subclass. return True # Sadly this is just some really fiddly logic to handle all the cases # of user-defined generic types, types inheriting from parametrised # generics, and so on. If you need to change this code, read PEP-560 # and Hypothesis issue #2951 closely first, and good luck. The tests # will help you, I hope - good luck. for orig_base in getattr(thing, "__orig_bases__", None) or [None]: args = getattr(orig_base, "__args__", None) if _compatible_args(args, superclass_args): return True return False except (AttributeError, TypeError): # Some types can't be the subject or object of an instance or subclass check return False def _evaluate_type_alias_type(thing, *, typevars): # pragma: no cover # 3.12+ if isinstance(thing, typing.TypeVar): if thing not in typevars: raise ValueError( f"Cannot look up value for unbound type var {thing}. " f"Bound typevars: {typevars}" ) return typevars[thing] origin = get_origin(thing) if origin is None: # not a parametrized type, so nothing to substitute. return thing args = get_args(thing) # we had an origin, so we must have an args # note: I'm only mostly confident this is true and there may be a subtle # violator. assert args concrete_args = tuple( _evaluate_type_alias_type(arg, typevars=typevars) for arg in args ) if isinstance(origin, typing.TypeAliasType): for param in origin.__type_params__: # there's no principled reason not to support these, they're just # annoying to implement. if isinstance(param, typing.TypeVarTuple): raise HypothesisException( f"Hypothesis does not yet support resolution for TypeVarTuple " f"{param} (in origin: {origin!r}). Please open an issue if " "you would like to see support for this." ) if isinstance(param, typing.ParamSpec): raise HypothesisException( f"Hypothesis does not yet support resolution for ParamSpec " f"{param} (in origin: {origin!r}). Please open an issue if you " "would like to see support for this." ) # this zip is non-strict to allow for e.g. # `type A[T1, T2] = list[T1]; st.from_type(A[int]).example()`, # which leaves T2 free but is still acceptable as it never references # it. # # We disallow referencing a free / unbound type var by erroring # elsewhere in this function. typevars |= dict(zip(origin.__type_params__, concrete_args, strict=False)) return _evaluate_type_alias_type(origin.__value__, typevars=typevars) return origin[concrete_args] def evaluate_type_alias_type(thing): # pragma: no cover # covered on 3.12+ # this function takes a GenericAlias whose origin is a TypeAliasType, # which corresponds to `type A[T] = list[T]; thing = A[int]`, and returns # the fully-instantiated underlying type. assert isinstance(thing, GenericAlias) assert is_a_type_alias_type(get_origin(thing)) return _evaluate_type_alias_type(thing, typevars={}) def is_a_type_alias_type(thing): # pragma: no cover # covered by 3.12+ tests # TypeAliasType is new in python 3.12, through the type statement. If we're # before python 3.12 then this can't possibly by a TypeAliasType. # # https://docs.python.org/3/reference/simple_stmts.html#type # https://docs.python.org/3/library/typing.html#typing.TypeAliasType if sys.version_info < (3, 12): return False return isinstance(thing, typing.TypeAliasType) def is_a_union(thing: object) -> bool: """Return True if thing is a typing.Union or types.UnionType (in py310).""" return isinstance(thing, UnionType) or get_origin(thing) is typing.Union def is_a_type(thing: object) -> bool: """ Return True if thing is a type or a typing-like thing (union, generic type, etc). """ return ( isinstance(thing, type) or is_generic_type(thing) or isinstance(thing, NewType) or is_a_type_alias_type(thing) # union and forwardref checks necessary from 3.14+. Before 3.14, they # were covered by is_generic_type(thing). or is_a_union(thing) or isinstance(thing, typing.ForwardRef) ) def is_typing_literal(thing: object) -> bool: return get_origin(thing) in ( typing.Literal, getattr(typing_extensions, "Literal", object()), ) def is_annotated_type(thing: object) -> bool: return ( isinstance(thing, _AnnotatedAlias) and getattr(thing, "__args__", None) is not None ) def get_constraints_filter_map(): if at := sys.modules.get("annotated_types"): return { # Due to the order of operator.gt/ge/lt/le arguments, order is inversed: at.Gt: lambda constraint: partial(operator.lt, constraint.gt), at.Ge: lambda constraint: partial(operator.le, constraint.ge), at.Lt: lambda constraint: partial(operator.gt, constraint.lt), at.Le: lambda constraint: partial(operator.ge, constraint.le), at.MinLen: lambda constraint: partial(min_len, constraint.min_length), at.MaxLen: lambda constraint: partial(max_len, constraint.max_length), at.Predicate: lambda constraint: constraint.func, } return {} # pragma: no cover def _get_constraints(args: tuple[Any, ...]) -> Iterator["at.BaseMetadata"]: at = sys.modules.get("annotated_types") for arg in args: if at and isinstance(arg, at.BaseMetadata): yield arg elif getattr(arg, "__is_annotated_types_grouped_metadata__", False): for subarg in arg: if getattr(subarg, "__is_annotated_types_grouped_metadata__", False): yield from _get_constraints(tuple(subarg)) else: yield subarg elif at and isinstance(arg, slice) and arg.step in (1, None): yield from at.Len(arg.start or 0, arg.stop) def _flat_annotated_repr_parts(annotated_type): # Helper to get a good error message in find_annotated_strategy() below. type_reps = [ get_pretty_function_description(a) for a in annotated_type.__args__ if not isinstance(a, typing.TypeVar) ] metadata_reps = [] for m in getattr(annotated_type, "__metadata__", ()): if is_annotated_type(m): ts, ms = _flat_annotated_repr_parts(m) type_reps.extend(ts) metadata_reps.extend(ms) else: metadata_reps.append(get_pretty_function_description(m)) return type_reps, metadata_reps def find_annotated_strategy(annotated_type): metadata = getattr(annotated_type, "__metadata__", ()) if any(is_annotated_type(arg) for arg in metadata): # Annotated[Annotated[T], ...] is perfectly acceptable, but it's all to easy # to instead write Annotated[T1, Annotated[T2, ...]] - and nobody else checks # for that at runtime. Once you add generics this can be seriously confusing, # so we go to some trouble to give a helpful error message. # For details: https://github.com/HypothesisWorks/hypothesis/issues/3891 ty_rep = repr(annotated_type).replace("typing.Annotated", "Annotated") ts, ms = _flat_annotated_repr_parts(annotated_type) bits = ", ".join([" | ".join(dict.fromkeys(ts or "?")), *dict.fromkeys(ms)]) raise ResolutionFailed( f"`{ty_rep}` is invalid because nesting Annotated is only allowed for " f"the first (type) argument, not for later (metadata) arguments. " f"Did you mean `Annotated[{bits}]`?" ) for arg in reversed(metadata): if isinstance(arg, st.SearchStrategy): return arg filter_conditions = [] unsupported = [] constraints_map = get_constraints_filter_map() for constraint in _get_constraints(metadata): if isinstance(constraint, st.SearchStrategy): return constraint if convert := constraints_map.get(type(constraint)): filter_conditions.append(convert(constraint)) else: unsupported.append(constraint) if unsupported: msg = f"Ignoring unsupported {', '.join(map(repr, unsupported))}" warnings.warn(msg, HypothesisWarning, stacklevel=2) base_strategy = st.from_type(annotated_type.__origin__) for filter_condition in filter_conditions: base_strategy = base_strategy.filter(filter_condition) return base_strategy def has_type_arguments(type_): """Decides whethere or not this type has applied type arguments.""" args = getattr(type_, "__args__", None) if args and isinstance(type_, (typing._GenericAlias, GenericAlias)): # There are some cases when declared types do already have type arguments # Like `Sequence`, that is `_GenericAlias(abc.Sequence[T])[T]` parameters = getattr(type_, "__parameters__", None) if parameters: # So, we need to know if type args are just "aliases" return args != parameters return bool(args) def is_generic_type(type_): """Decides whether a given type is generic or not.""" # The ugly truth is that `MyClass`, `MyClass[T]`, and `MyClass[int]` are very different. # We check for `MyClass[T]` and `MyClass[int]` with the first condition, # while the second condition is for `MyClass`. return isinstance(type_, (*typing_root_type, GenericAlias)) or ( isinstance(type_, type) and (typing.Generic in type_.__mro__ or hasattr(type_, "__class_getitem__")) ) __EVAL_TYPE_TAKES_TYPE_PARAMS = ( "type_params" in inspect.signature(typing._eval_type).parameters # type: ignore ) def _try_import_forward_ref(thing, typ, *, type_params): # pragma: no cover """ Tries to import a real bound or default type from ``ForwardRef`` in ``TypeVar``. This function is very "magical" to say the least, please don't use it. This function fully covered, but is excluded from coverage because we can only cover each path in a separate python version. """ try: kw = {"globalns": vars(sys.modules[thing.__module__]), "localns": None} if __EVAL_TYPE_TAKES_TYPE_PARAMS: kw["type_params"] = type_params return typing._eval_type(typ, **kw) except (KeyError, AttributeError, NameError): # We fallback to `ForwardRef` instance, you can register it as a type as well: # >>> from typing import ForwardRef # >>> from hypothesis import strategies as st # >>> st.register_type_strategy(ForwardRef('YourType'), your_strategy) return typ def from_typing_type(thing): # We start with Final, Literal, and Annotated, since they don't support `isinstance`. # # We then explicitly error on non-Generic types, which don't carry enough # information to sensibly resolve to strategies at runtime. # Finally, we run a variation of the subclass lookup in `st.from_type` # among generic types in the lookup. if get_origin(thing) == typing.Final: return st.one_of([st.from_type(t) for t in thing.__args__]) if is_typing_literal(thing): args_dfs_stack = list(thing.__args__) literals = [] while args_dfs_stack: arg = args_dfs_stack.pop() if is_typing_literal(arg): # pragma: no cover # Python 3.10+ flattens for us when constructing Literal objects args_dfs_stack.extend(reversed(arg.__args__)) else: literals.append(arg) return st.sampled_from(literals) if is_annotated_type(thing): return find_annotated_strategy(thing) # Some "generic" classes are not generic *in* anything - for example both # Hashable and Sized have `__args__ == ()` origin = get_origin(thing) or thing if ( origin in vars(collections.abc).values() and len(getattr(thing, "__args__", None) or []) == 0 ): return st.from_type(origin) # Parametrised generic types have their __origin__ attribute set to the # un-parametrised version, which we need to use in the subclass checks. # i.e.: typing.List[int].__origin__ == list mapping = { k: v for k, v in _global_type_lookup.items() if is_generic_type(k) and try_issubclass(k, thing) } # Discard any type which is not it's own origin, where the origin is also in the # mapping. On old Python versions this could be due to redefinition of types # between collections.abc and typing, but the logic seems reasonable to keep in # case of similar situations now that's been fixed. for t in sorted(mapping, key=type_sorting_key): origin = get_origin(t) if origin is not t and origin in mapping: mapping.pop(t) # Drop some unusual cases for simplicity, including tuples or its # subclasses (e.g. namedtuple) if len(mapping) > 1: _Environ = getattr(os, "_Environ", None) mapping.pop(_Environ, None) tuple_types = [ t for t in mapping if (isinstance(t, type) and issubclass(t, tuple)) or get_origin(t) is tuple ] if len(mapping) > len(tuple_types): for tuple_type in tuple_types: mapping.pop(tuple_type) if {dict, set}.intersection(mapping): # ItemsView can cause test_lookup.py::test_specialised_collection_types # to fail, due to weird isinstance behaviour around the elements. mapping.pop(collections.abc.ItemsView, None) mapping.pop(typing.ItemsView, None) if collections.deque in mapping and len(mapping) > 1: # Resolving generic sequences to include a deque is more trouble for e.g. # the ghostwriter than it's worth, via undefined names in the repr. mapping.pop(collections.deque) if ( memoryview in mapping and getattr(thing, "__args__", None) and not hasattr(thing.__args__[0], "__buffer__") ): # pragma: no cover # covered by 3.14+ # Both memoryview and list are direct subclasses of Sequence. If we ask for # st.from_type(Sequence[A]), we will get both list[A] and memoryview[A]. # But unless A implements the buffer protocol with __buffer__, resolving # memoryview[A] will error. # # Since the user didn't explicitly ask for memoryview, there's no reason # to expect them to have implemented __buffer__. Remove memoryview in this # case, before it can fail at resolution-time. # # Note: I intentionally did not add a `and len(mapping) > 1` condition here. # If memoryview[A] is the only resolution for a strategy, but A is not a # buffer protocol, our options are to (1) pop memoryview and raise # ResolutionFailed, or (2) to keep memoryview in the mapping and error in # resolve_memoryview. A failure in test_resolving_standard_contextmanager_as_generic # (because memoryview is a context manager in 3.14) convinced me the former # was less confusing to users. mapping.pop(memoryview) elem_type = (getattr(thing, "__args__", None) or ["not int"])[0] union_elems = elem_type.__args__ if is_a_union(elem_type) else () allows_integer_elements = any( isinstance(T, type) and try_issubclass(int, get_origin(T) or T) for T in [*union_elems, elem_type] ) if len(mapping) > 1: # issubclass treats bytestring as a kind of sequence, which it is, # but treating it as such breaks everything else when it is presumed # to be a generic sequence or container that could hold any item. # Except for sequences of integers, or unions which include integer! # See https://github.com/HypothesisWorks/hypothesis/issues/2257 # # This block drops bytes from the types that can be generated # if there is more than one allowed type, and the element type is # not either `int` or a Union with `int` as one of its elements. if not allows_integer_elements: mapping.pop(bytes, None) if sys.version_info[:2] <= (3, 13): mapping.pop(collections.abc.ByteString, None) elif ( (not mapping) and isinstance(thing, typing.ForwardRef) and thing.__forward_arg__ in vars(builtins) ): return st.from_type(getattr(builtins, thing.__forward_arg__)) def is_maximal(t): # For each k in the mapping, we use it if it's the most general type # available, and exclude any more specific types. So if both # Sequence and Collection are available, we use the most general Collection # type. # # k being "the most general" is equivalent to saying that k is maximal # in the partial ordering of types. Note that since the ordering is # partial there may be multiple maximal elements. (This distinguishes # maximal from maximum). return sum(try_issubclass(t, T) for T in mapping) == 1 strategies = [ (t, s if isinstance(s, st.SearchStrategy) else s(thing)) for t, s in mapping.items() if is_maximal(t) ] strategies = [(t, s) for t, s in strategies if s != NotImplemented] # 3.14+ removes typing.ByteString. typing.ByteString was the only reason we # previously generated bytes for Sequence[int]. There is no equivalent # for typing.ByteString in 3.14+, but we would still like to generate bytes # for Sequence[int] and its supertypes. Special case that here. if ( sys.version_info[:2] >= (3, 14) and allows_integer_elements # For the same reason as the is_maximal check above, we only include # this ByteString special case if it is not overridden by a more general # available type. # # collections.abc.ByteString was a direct subclass of Sequence, so we # use that as the standin type when checking. Note we compare to a count # of 0, instead of 1, since in is_maximal `k` is already in `mapping`, # and we expect `try_issubclass(k, k) == True`. and try_issubclass(collections.abc.Sequence, thing) and sum(try_issubclass(collections.abc.Sequence, T) for T in mapping) == 0 ): # pragma: no cover # covered on 3.14+ strategies.append((collections.abc.Sequence, st.binary())) # Sort strategies according to our type-sorting heuristic for stable output strategies = [ s for _k, s in sorted(strategies, key=lambda kv: type_sorting_key(kv[0])) ] empty = ", ".join(repr(s) for s in strategies if s.is_empty) if empty or not strategies: raise ResolutionFailed( f"Could not resolve {empty or thing} to a strategy; " "consider using register_type_strategy" ) return st.one_of(strategies) def can_cast(type, value): """Determine if value can be cast to type.""" try: type(value) return True except Exception: return False def _networks(bits): return st.tuples(st.integers(0, 2**bits - 1), st.integers(-bits, 0).map(abs)) utc_offsets = st.builds( datetime.timedelta, minutes=st.integers(0, 59), hours=st.integers(-23, 23) ) # These builtin and standard-library types have Hypothesis strategies, # seem likely to appear in type annotations, or are otherwise notable. # # The strategies below must cover all possible values from the type, because # many users treat them as comprehensive and one of Hypothesis' design goals # is to avoid testing less than expected. # # As a general rule, we try to limit this to scalars because from_type() # would have to decide on arbitrary collection elements, and we'd rather # not (with typing module generic types and some builtins as exceptions). # # Strategy Callables may return NotImplemented, which should be treated in the # same way as if the type was not registered. # # Note that NotImplemented cannot be typed in Python 3.8 because there's no type # exposed for it, and NotImplemented itself is typed as Any so that it can be # returned without being listed in a function signature: # https://github.com/python/mypy/issues/6710#issuecomment-485580032 if sys.version_info < (3, 12): _RegistryKeyT: typing.TypeAlias = type else: # pragma: no cover _RegistryKeyT: typing.TypeAlias = type | typing.TypeAliasType _global_type_lookup: dict[ _RegistryKeyT, st.SearchStrategy | typing.Callable[[type], st.SearchStrategy] ] = { type(None): st.none(), bool: st.booleans(), int: st.integers(), float: st.floats(), complex: st.complex_numbers(), fractions.Fraction: st.fractions(), decimal.Decimal: st.decimals(), str: st.text(), bytes: st.binary(), datetime.datetime: st.datetimes(), datetime.date: st.dates(), datetime.time: st.times(), datetime.timedelta: st.timedeltas(), datetime.timezone: st.builds(datetime.timezone, offset=utc_offsets) | st.builds(datetime.timezone, offset=utc_offsets, name=st.text(st.characters())), uuid.UUID: st.uuids(), tuple: st.builds(tuple), list: st.builds(list), set: st.builds(set), collections.abc.MutableSet: st.builds(set), frozenset: st.builds(frozenset), dict: st.builds(dict), FunctionType: st.functions(), type(Ellipsis): st.just(Ellipsis), type(NotImplemented): st.just(NotImplemented), bytearray: st.binary().map(bytearray), numbers.Real: st.floats(), numbers.Rational: st.fractions(), numbers.Number: st.complex_numbers(), numbers.Integral: st.integers(), numbers.Complex: st.complex_numbers(), slice: st.builds( slice, st.none() | st.integers(), st.none() | st.integers(), st.none() | st.integers(), ), range: st.one_of( st.builds(range, st.integers(min_value=0)), st.builds(range, st.integers(), st.integers()), st.builds(range, st.integers(), st.integers(), st.integers().filter(bool)), ), ipaddress.IPv4Address: ip_addresses(v=4), ipaddress.IPv6Address: ip_addresses(v=6), ipaddress.IPv4Interface: _networks(32).map(ipaddress.IPv4Interface), ipaddress.IPv6Interface: _networks(128).map(ipaddress.IPv6Interface), ipaddress.IPv4Network: st.one_of( _networks(32).map(lambda x: ipaddress.IPv4Network(x, strict=False)), st.sampled_from(SPECIAL_IPv4_RANGES).map(ipaddress.IPv4Network), ), ipaddress.IPv6Network: st.one_of( _networks(128).map(lambda x: ipaddress.IPv6Network(x, strict=False)), st.sampled_from(SPECIAL_IPv6_RANGES).map(ipaddress.IPv6Network), ), os.PathLike: st.builds(PurePath, st.text()), UnicodeDecodeError: st.builds( UnicodeDecodeError, st.just("unknown encoding"), st.just(b""), st.just(0), st.just(0), st.just("reason"), ), UnicodeEncodeError: st.builds( UnicodeEncodeError, st.just("unknown encoding"), st.text(), st.just(0), st.just(0), st.just("reason"), ), UnicodeTranslateError: st.builds( UnicodeTranslateError, st.text(), st.just(0), st.just(0), st.just("reason") ), BaseExceptionGroup: st.builds( BaseExceptionGroup, st.text(), st.lists(st.from_type(BaseException), min_size=1, max_size=5), ), ExceptionGroup: st.builds( ExceptionGroup, st.text(), st.lists(st.from_type(Exception), min_size=1, max_size=5), ), enumerate: st.builds(enumerate, st.just(())), filter: st.builds(filter, st.just(lambda _: None), st.just(())), map: st.builds(map, st.just(lambda _: None), st.just(())), reversed: st.builds(reversed, st.just(())), zip: st.builds(zip), # avoids warnings on PyPy 7.3.14+ property: st.builds(property, st.just(lambda _: None)), classmethod: st.builds(classmethod, st.just(lambda self: self)), staticmethod: st.builds(staticmethod, st.just(lambda self: self)), super: st.builds(super, st.from_type(type)), re.Match: st.text().map(lambda c: re.match(".", c, flags=re.DOTALL)).filter(bool), re.Pattern: st.builds(re.compile, st.sampled_from(["", b""])), random.Random: st.randoms(), zoneinfo.ZoneInfo: st.timezones(), # Pull requests with more types welcome! } if PYPY: _global_type_lookup[builtins.sequenceiterator] = st.builds(iter, st.tuples()) # type: ignore _fallback_type_strategy = st.sampled_from( sorted(_global_type_lookup, key=type_sorting_key) ) # subclass of MutableMapping, and so we resolve to a union which # includes this... but we don't actually ever want to build one. _global_type_lookup[os._Environ] = st.just(os.environ) if sys.version_info[:2] < (3, 14): # Note: while ByteString notionally also represents the bytearray and # memoryview types, it is a subclass of Hashable and those types are not. # We therefore only generate the bytes type. type-ignored due to deprecation. _global_type_lookup[typing.ByteString] = st.binary() # type: ignore _global_type_lookup[collections.abc.ByteString] = st.binary() # type: ignore _global_type_lookup[memoryview] = st.binary().map(memoryview) _global_type_lookup.update( { # TODO: SupportsAbs and SupportsRound should be covariant, ie have functions. typing.SupportsAbs: st.one_of( st.booleans(), st.integers(), st.floats(), st.complex_numbers(), st.fractions(), st.decimals(), st.timedeltas(), ), typing.SupportsRound: st.one_of( st.booleans(), st.integers(), st.floats(), st.decimals(), st.fractions() ), typing.SupportsComplex: st.one_of( st.booleans(), st.integers(), st.floats(), st.complex_numbers(), st.decimals(), st.fractions(), ), typing.SupportsFloat: st.one_of( st.booleans(), st.integers(), st.floats(), st.decimals(), st.fractions(), # with floats its far more annoying to capture all # the magic in a regex. so we just stringify some floats st.floats().map(str), ), typing.SupportsInt: st.one_of( st.booleans(), st.integers(), st.floats(), st.uuids(), st.decimals(), # this generates strings that should able to be parsed into integers st.from_regex(r"\A-?\d+\Z").filter(functools.partial(can_cast, int)), ), typing.SupportsIndex: st.integers() | st.booleans(), typing.SupportsBytes: st.one_of( st.booleans(), st.binary(), st.integers(0, 255), # As with Reversible, we tuplize this for compatibility with Hashable. st.lists(st.integers(0, 255)).map(tuple), ), typing.BinaryIO: st.builds(io.BytesIO, st.binary()), typing.TextIO: st.builds(io.StringIO, st.text()), } ) # The "extra" lookups define a callable that either resolves to a strategy for # this narrowly extra-specific type, or returns None to proceed with normal # type resolution. The callable will only be called if the module is # installed. To avoid the performance hit of importing anything here, we defer # it until the method is called the first time, at which point we replace the # entry in the lookup table with the direct call. def _from_numpy_type(thing: type) -> st.SearchStrategy | None: from hypothesis.extra.numpy import _from_type _global_extra_lookup["numpy"] = _from_type return _from_type(thing) _global_extra_lookup: dict[str, typing.Callable[[type], st.SearchStrategy | None]] = { "numpy": _from_numpy_type, } def register(type_, fallback=None, *, module=typing): if isinstance(type_, str): # Use the name of generic types which are not available on all # versions, and the function just won't be added to the registry; # also works when module=None because typing_extensions isn't # installed (nocover because it _is_ in our coverage tests). type_ = getattr(module, type_, None) if type_ is None: # pragma: no cover return lambda f: f def inner(func): nonlocal type_ if fallback is None: _global_type_lookup[type_] = func return func @functools.wraps(func) def really_inner(thing): if getattr(thing, "__args__", None) is None: return fallback return func(thing) _global_type_lookup[type_] = really_inner _global_type_lookup[get_origin(type_) or type_] = really_inner return really_inner return inner @register(type) @register("Type") @register("Type", module=typing_extensions) def resolve_Type(thing): if getattr(thing, "__args__", None) is None or get_args(thing) == (): return _fallback_type_strategy args = (thing.__args__[0],) if is_a_union(args[0]): args = args[0].__args__ # Duplicate check from from_type here - only paying when needed. args = list(args) for i, a in enumerate(args): if type(a) in (typing.ForwardRef, str): try: args[i] = getattr(builtins, getattr(a, "__forward_arg__", a)) except AttributeError: raise ResolutionFailed( f"Cannot find the type referenced by {thing} - try using " f"st.register_type_strategy({thing}, st.from_type(...))" ) from None return st.sampled_from(sorted(args, key=type_sorting_key)) @register("List", st.builds(list)) def resolve_List(thing): return st.lists(st.from_type(thing.__args__[0])) @register("Tuple", st.builds(tuple)) def resolve_Tuple(thing): elem_types = getattr(thing, "__args__", None) or () if len(elem_types) == 2 and elem_types[-1] is Ellipsis: return st.lists(st.from_type(elem_types[0])).map(tuple) elif len(elem_types) == 1 and elem_types[0] == (): # pragma: no cover # Empty tuple; see issue #1583. # Only possible on 3.10. `from typing import Tuple; Tuple[()].__args__` # is ((),) on 3.10, and () on 3.11+. return st.tuples() return st.tuples(*map(st.from_type, elem_types)) def _can_hash(val): try: hash(val) return True except Exception: return False # Some types are subclasses of typing.Hashable, because they define a __hash__ # method, but have non-hashable instances such as `Decimal("snan")` or may contain # such instances (e.g. `FrozenSet[Decimal]`). We therefore keep this whitelist of # types which are always hashable, and apply the `_can_hash` filter to all others. # Our goal is not completeness, it's to get a small performance boost for the most # common cases, and a short whitelist is basically free to maintain. ALWAYS_HASHABLE_TYPES = {type(None), bool, int, float, complex, str, bytes} def _from_hashable_type(type_): if type_ in ALWAYS_HASHABLE_TYPES: return st.from_type(type_) else: return st.from_type(type_).filter(_can_hash) @register("Set", st.builds(set)) @register(typing.MutableSet, st.builds(set)) def resolve_Set(thing): return st.sets(_from_hashable_type(thing.__args__[0])) @register("FrozenSet", st.builds(frozenset)) def resolve_FrozenSet(thing): return st.frozensets(_from_hashable_type(thing.__args__[0])) @register("Dict", st.builds(dict)) def resolve_Dict(thing): # If thing is a Collection instance, we need to fill in the values keys, vals, *_ = thing.__args__ * 2 return st.dictionaries( _from_hashable_type(keys), st.none() if vals is None else st.from_type(vals), ) @register("DefaultDict", st.builds(collections.defaultdict)) @register("DefaultDict", st.builds(collections.defaultdict), module=typing_extensions) def resolve_DefaultDict(thing): return resolve_Dict(thing).map(lambda d: collections.defaultdict(None, d)) @register(typing.ItemsView, st.builds(dict).map(dict.items)) def resolve_ItemsView(thing): return resolve_Dict(thing).map(dict.items) @register(typing.KeysView, st.builds(dict).map(dict.keys)) def resolve_KeysView(thing): return st.dictionaries(_from_hashable_type(thing.__args__[0]), st.none()).map( dict.keys ) @register(typing.ValuesView, st.builds(dict).map(dict.values)) def resolve_ValuesView(thing): return st.dictionaries(st.integers(), st.from_type(thing.__args__[0])).map( dict.values ) @register(typing.Iterator, st.iterables(st.nothing())) def resolve_Iterator(thing): return st.iterables(st.from_type(thing.__args__[0])) @register(collections.Counter, st.builds(collections.Counter)) def resolve_Counter(thing): return st.dictionaries( keys=st.from_type(thing.__args__[0]), values=st.integers(), ).map(collections.Counter) @register(collections.deque, st.builds(collections.deque)) def resolve_deque(thing): return st.lists(st.from_type(thing.__args__[0])).map(collections.deque) @register(collections.ChainMap, st.builds(dict).map(collections.ChainMap)) def resolve_ChainMap(thing): return resolve_Dict(thing).map(collections.ChainMap) @register(collections.OrderedDict, st.builds(dict).map(collections.OrderedDict)) def resolve_OrderedDict(thing): return resolve_Dict(thing).map(collections.OrderedDict) @register(typing.Pattern, st.builds(re.compile, st.sampled_from(["", b""]))) def resolve_Pattern(thing): if isinstance(thing.__args__[0], typing.TypeVar): # pragma: no cover # FIXME: this was covered on Python 3.8, but isn't on 3.10 - we should # work out why not and write some extra tests to help avoid regressions. return st.builds(re.compile, st.sampled_from(["", b""])) return st.just(re.compile(thing.__args__[0]())) @register( typing.Match, st.text().map(partial(re.match, ".", flags=re.DOTALL)).filter(bool), ) def resolve_Match(thing): if thing.__args__[0] == bytes: return ( st.binary(min_size=1) .map(lambda c: re.match(b".", c, flags=re.DOTALL)) .filter(bool) ) return st.text().map(lambda c: re.match(".", c, flags=re.DOTALL)).filter(bool) class GeneratorStrategy(st.SearchStrategy): def __init__(self, yields, returns): super().__init__() assert isinstance(yields, st.SearchStrategy) assert isinstance(returns, st.SearchStrategy) self.yields = yields self.returns = returns def __repr__(self) -> str: return f"" def do_draw(self, data): elements = conjecture_utils_many(data, min_size=0, max_size=100, average_size=5) while elements.more(): yield data.draw(self.yields) return data.draw(self.returns) @register(typing.Generator, GeneratorStrategy(st.none(), st.none())) def resolve_Generator(thing): yields, _, returns = thing.__args__ return GeneratorStrategy(st.from_type(yields), st.from_type(returns)) @register(typing.Callable, st.functions()) def resolve_Callable(thing): # Generated functions either accept no arguments, or arbitrary arguments. # This is looser than ideal, but anything tighter would generally break # use of keyword arguments and we'd rather not force positional-only. if not thing.__args__: # pragma: no cover # varies by minor version return st.functions() *args_types, return_type = thing.__args__ # Note that a list can only appear in __args__ under Python 3.9 with the # collections.abc version; see https://bugs.python.org/issue42195 if len(args_types) == 1 and isinstance(args_types[0], list): args_types = tuple(args_types[0]) # pragma: no cover pep612 = ConcatenateTypes + ParamSpecTypes for arg in args_types: # awkward dance because you can't use Concatenate in isistance or issubclass if getattr(arg, "__origin__", arg) in pep612 or type(arg) in pep612: raise InvalidArgument( "Hypothesis can't yet construct a strategy for instances of a " f"Callable type parametrized by {arg!r}. Consider using an " "explicit strategy, or opening an issue." ) if get_origin(return_type) in TypeGuardTypes: raise InvalidArgument( "Hypothesis cannot yet construct a strategy for callables which " f"are PEP-647 TypeGuards or PEP-742 TypeIs (got {return_type!r}). " "Consider using an explicit strategy, or opening an issue." ) if get_origin(thing) is collections.abc.Callable and return_type is None: return_type = type(None) return st.functions( like=(lambda *a, **k: None) if args_types else (lambda: None), returns=st.from_type(return_type), ) @register(typing.TypeVar) @register("TypeVar", module=typing_extensions) def resolve_TypeVar(thing): type_var_key = f"typevar={thing!r}" bound = getattr(thing, "__bound__", None) default = getattr(thing, "__default__", NoDefaults[0]) original_strategies = [] def resolve_strategies(typ): if isinstance(typ, typing.ForwardRef): # TODO: on Python 3.13 and later, we should work out what type_params # could be part of this type, and pass them in here. typ = _try_import_forward_ref(thing, typ, type_params=()) strat = unwrap_strategies(st.from_type(typ)) if not isinstance(strat, OneOfStrategy): original_strategies.append(strat) else: original_strategies.extend(strat.original_strategies) if bound is not None: resolve_strategies(bound) if default not in NoDefaults: # pragma: no cover # Coverage requires 3.13 or `typing_extensions` package. resolve_strategies(default) if original_strategies: # The bound / default was a union, or we resolved it as a union of subtypes, # so we need to unpack the strategy to ensure consistency across uses. # This incantation runs a sampled_from over the strategies inferred for # each part of the union, wraps that in shared so that we only generate # from one type per testcase, and flatmaps that back to instances. return st.shared( st.sampled_from(original_strategies), key=type_var_key ).flatmap(lambda s: s) builtin_scalar_types = [type(None), bool, int, float, str, bytes] return st.shared( st.sampled_from( # Constraints may be None or () on various Python versions. getattr(thing, "__constraints__", None) or builtin_scalar_types, ), key=type_var_key, ).flatmap(st.from_type) if sys.version_info[:2] >= (3, 14): # memoryview is newly generic in 3.14. see # https://github.com/python/cpython/issues/126012 # and https://docs.python.org/3/library/stdtypes.html#memoryview @register(memoryview, st.binary().map(memoryview)) def resolve_memoryview(thing): return st.from_type(thing.__args__[0]).map(memoryview) ================================================ FILE: hypothesis-python/src/hypothesis/strategies/_internal/utils.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import dataclasses import sys from collections.abc import Callable from functools import partial from typing import Literal, TypeAlias, TypeVar from weakref import WeakValueDictionary from hypothesis.errors import InvalidArgument from hypothesis.internal.cache import LRUReusedCache from hypothesis.internal.floats import clamp, float_to_int from hypothesis.internal.reflection import proxies from hypothesis.vendor.pretty import pretty T = TypeVar("T") ValueKey: TypeAlias = tuple[type, object] # (fn, args, kwargs) StrategyCacheKey: TypeAlias = tuple[ object, tuple[ValueKey, ...], frozenset[tuple[str, ValueKey]] ] _all_strategies: WeakValueDictionary[str, Callable] = WeakValueDictionary() # note: LRUReusedCache is already thread-local internally _STRATEGY_CACHE = LRUReusedCache[StrategyCacheKey, object](1024) def _value_key(value: object) -> ValueKey: if isinstance(value, float): return (float, float_to_int(value)) return (type(value), value) def clear_strategy_cache() -> None: _STRATEGY_CACHE.clear() def cacheable(fn: T) -> T: from hypothesis.control import _current_build_context from hypothesis.strategies._internal.strategies import SearchStrategy @proxies(fn) def cached_strategy(*args, **kwargs): context = _current_build_context.value if context is not None and context.data.provider.avoid_realization: return fn(*args, **kwargs) try: kwargs_cache_key = {(k, _value_key(v)) for k, v in kwargs.items()} except TypeError: return fn(*args, **kwargs) cache_key = ( fn, tuple(_value_key(v) for v in args), frozenset(kwargs_cache_key), ) try: return _STRATEGY_CACHE[cache_key] except KeyError: pass except TypeError: return fn(*args, **kwargs) result = fn(*args, **kwargs) if not isinstance(result, SearchStrategy) or result.is_cacheable: _STRATEGY_CACHE[cache_key] = result return result # note that calling this clears the full _STRATEGY_CACHE for all strategies, # not just the cache for this strategy. cached_strategy.__clear_cache = clear_strategy_cache # type: ignore return cached_strategy def defines_strategy( *, force_reusable_values: bool = False, eager: bool | Literal["try"] = False, ) -> Callable[[T], T]: """ Each standard strategy function provided to users by Hypothesis should be decorated with @defines_strategy. This registers the strategy with _all_strategies, which is used in our own test suite to check that e.g. we document all strategies in sphinx. If you're reading this and are the author of a third-party strategy library: don't worry, third-party strategies don't need to be decorated with @defines_strategy. This function is internal to Hypothesis and not intended for outside use. Parameters ---------- force_reusable_values : bool If ``True``, strategies returned from the strategy function will have ``.has_reusable_values == True`` set, even if it uses maps/filters or non-reusable strategies internally. This tells our numpy/pandas strategies that they can implicitly use such strategies as background values. eager : bool | "try" If ``True``, strategies returned by the strategy function are returned as-is, and not wrapped in LazyStrategy. If "try", we first attempt to call the strategy function and return the resulting strategy. If this throws an exception, we treat it the same as ``eager = False``, by returning the strategy function wrapped in a LazyStrategy. """ if eager is not False and force_reusable_values: # pragma: no cover # We could support eager + force_reusable_values with a suitable wrapper, # but there are currently no callers that request this combination. raise InvalidArgument( f"Passing both eager={eager} and force_reusable_values=True is " "currently not supported" ) def decorator(strategy_definition): _all_strategies[strategy_definition.__name__] = strategy_definition if eager is True: return strategy_definition @proxies(strategy_definition) def accept(*args, **kwargs): from hypothesis.strategies._internal.lazy import LazyStrategy if eager == "try": # Why not try this unconditionally? Because we'd end up with very # deep nesting of recursive strategies - better to be lazy unless we # *know* that eager evaluation is the right choice. try: return strategy_definition(*args, **kwargs) except Exception: # If invoking the strategy definition raises an exception, # wrap that up in a LazyStrategy so it happens again later. pass result = LazyStrategy(strategy_definition, args, kwargs) if force_reusable_values: # Setting `force_has_reusable_values` here causes the recursive # property code to set `.has_reusable_values == True`. result.force_has_reusable_values = True assert result.has_reusable_values return result accept.is_hypothesis_strategy_function = True return accept return decorator def _to_jsonable(obj: object, *, avoid_realization: bool, seen: set[int]) -> object: if isinstance(obj, (str, int, float, bool, type(None))): # We convert integers of 2**63 to floats, to avoid crashing external # utilities with a 64 bit integer cap (notable, sqlite). See # https://github.com/HypothesisWorks/hypothesis/pull/3797#discussion_r1413425110 # and https://github.com/simonw/sqlite-utils/issues/605. if isinstance(obj, int) and not isinstance(obj, bool) and abs(obj) >= 2**63: # Silently clamp very large ints to max_float, to avoid OverflowError when # casting to float. (but avoid adding more constraints to symbolic values) if avoid_realization: return "" obj = clamp(-sys.float_info.max, obj, sys.float_info.max) return float(obj) return obj if avoid_realization: return "" obj_id = id(obj) if obj_id in seen: return pretty(obj, cycle=True) recur = partial( _to_jsonable, avoid_realization=avoid_realization, seen=seen | {obj_id} ) if isinstance(obj, (list, tuple, set, frozenset)): if isinstance(obj, tuple) and hasattr(obj, "_asdict"): return recur(obj._asdict()) # treat namedtuples as dicts return [recur(x) for x in obj] if isinstance(obj, dict): return { k if isinstance(k, str) else pretty(k): recur(v) for k, v in obj.items() } # Hey, might as well try calling a .to_json() method - it works for Pandas! # We try this before the below general-purpose handlers to give folks a # chance to control this behavior on their custom classes. try: return recur(obj.to_json()) # type: ignore except Exception: pass # Special handling for dataclasses, attrs, and pydantic classes if dataclasses.is_dataclass(obj) and not isinstance(obj, type): # Avoid dataclasses.asdict here to ensure that inner to_json overrides # can get called as well return { field.name: recur(getattr(obj, field.name)) for field in dataclasses.fields(obj) } if (attr := sys.modules.get("attr")) is not None and attr.has(type(obj)): return recur(attr.asdict(obj, recurse=False)) if (pyd := sys.modules.get("pydantic")) and isinstance(obj, pyd.BaseModel): return recur(obj.model_dump()) # If all else fails, we'll just pretty-print as a string. return pretty(obj) def to_jsonable(obj: object, *, avoid_realization: bool) -> object: """Recursively convert an object to json-encodable form. This is not intended to round-trip, but rather provide an analysis-ready format for observability. To avoid side affects, we pretty-print all but known types. """ return _to_jsonable(obj, avoid_realization=avoid_realization, seen=set()) ================================================ FILE: hypothesis-python/src/hypothesis/utils/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """hypothesis.utils is a package for things that you can consider part of the semi-public Hypothesis API but aren't really the core point.""" ================================================ FILE: hypothesis-python/src/hypothesis/utils/conventions.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. class UniqueIdentifier: """A factory for sentinel objects with nice reprs.""" def __init__(self, identifier: str) -> None: self.identifier = identifier def __repr__(self) -> str: return self.identifier infer = ... not_set = UniqueIdentifier("not_set") ================================================ FILE: hypothesis-python/src/hypothesis/utils/deprecation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime import warnings from hypothesis.errors import HypothesisDeprecationWarning def note_deprecation( message: str, *, since: str, has_codemod: bool, stacklevel: int = 0 ) -> None: if since != "RELEASEDAY": date = datetime.date.fromisoformat(since) assert datetime.date(2021, 1, 1) <= date if has_codemod: message += ( "\n The `hypothesis codemod` command-line tool can automatically " "refactor your code to fix this warning." ) warnings.warn(HypothesisDeprecationWarning(message), stacklevel=2 + stacklevel) ================================================ FILE: hypothesis-python/src/hypothesis/utils/dynamicvariables.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import threading from collections.abc import Generator from contextlib import contextmanager from typing import Generic, TypeVar T = TypeVar("T") class DynamicVariable(Generic[T]): def __init__(self, default: T) -> None: self.default = default self.data = threading.local() @property def value(self) -> T: return getattr(self.data, "value", self.default) @value.setter def value(self, value: T) -> None: self.data.value = value @contextmanager def with_value(self, value: T) -> Generator[None, None, None]: old_value = self.value try: self.data.value = value yield finally: self.data.value = old_value ================================================ FILE: hypothesis-python/src/hypothesis/utils/terminal.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os from typing import Literal def guess_background_color() -> Literal["light", "dark", "unknown"]: """Returns one of "dark", "light", or "unknown". This is basically just guessing, but better than always guessing "dark"! See also https://stackoverflow.com/questions/2507337/ and https://unix.stackexchange.com/questions/245378/ """ django_colors = os.getenv("DJANGO_COLORS", "") for theme in ("light", "dark"): if theme in django_colors.split(";"): return theme # Guessing based on the $COLORFGBG environment variable try: fg, *_, bg = os.getenv("COLORFGBG", "").split(";") except Exception: pass else: # 0=black, 7=light-grey, 15=white ; we don't interpret other colors if fg in ("7", "15") and bg == "0": return "dark" elif fg == "0" and bg in ("7", "15"): return "light" # TODO: Guessing based on the xterm control sequence return "unknown" ================================================ FILE: hypothesis-python/src/hypothesis/utils/threading.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import threading from collections.abc import Callable from typing import Any class ThreadLocal: """ Manages thread-local state. ThreadLocal forwards getattr and setattr to a threading.local() instance. The passed kwargs defines the available attributes on the threadlocal and their default values. The only supported names to geattr and setattr are the keys of the passed kwargs. """ def __init__(self, **kwargs: Callable) -> None: for name, value in kwargs.items(): if not callable(value): raise TypeError(f"Attribute {name} must be a callable. Got {value}") self.__initialized = False self.__kwargs = kwargs self.__threadlocal = threading.local() self.__initialized = True def __getattr__(self, name: str) -> Any: if name not in self.__kwargs: raise AttributeError(f"No attribute {name}") if not hasattr(self.__threadlocal, name): default = self.__kwargs[name]() setattr(self.__threadlocal, name, default) return getattr(self.__threadlocal, name) def __setattr__(self, name: str, value: Any) -> None: # disable attribute-forwarding while initializing if "_ThreadLocal__initialized" not in self.__dict__ or not self.__initialized: super().__setattr__(name, value) else: if name not in self.__kwargs: raise AttributeError(f"No attribute {name}") setattr(self.__threadlocal, name, value) ================================================ FILE: hypothesis-python/src/hypothesis/vendor/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/src/hypothesis/vendor/pretty.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ Python advanced pretty printer. This pretty printer is intended to replace the old `pprint` python module which does not allow developers to provide their own pretty print callbacks. This module is based on ruby's `prettyprint.rb` library by `Tanaka Akira`. Example Usage ------------- To get a string of the output use `pretty`:: from pretty import pretty string = pretty(complex_object) Extending --------- The pretty library allows developers to add pretty printing rules for their own objects. This process is straightforward. All you have to do is to add a `_repr_pretty_` method to your object and call the methods on the pretty printer passed:: class MyObject(object): def _repr_pretty_(self, p, cycle): ... Here is an example implementation of a `_repr_pretty_` method for a list subclass:: class MyList(list): def _repr_pretty_(self, p, cycle): if cycle: p.text('MyList(...)') else: with p.group(8, 'MyList([', '])'): for idx, item in enumerate(self): if idx: p.text(',') p.breakable() p.pretty(item) The `cycle` parameter is `True` if pretty detected a cycle. You *have* to react to that or the result is an infinite loop. `p.text()` just adds non breaking text to the output, `p.breakable()` either adds a whitespace or breaks here. If you pass it an argument it's used instead of the default space. `p.pretty` prettyprints another object using the pretty print method. The first parameter to the `group` function specifies the extra indentation of the next line. In this example the next item will either be on the same line (if the items are short enough) or aligned with the right edge of the opening bracket of `MyList`. If you just want to indent something you can use the group function without open / close parameters. You can also use this code:: with p.indent(2): ... Inheritance diagram: .. inheritance-diagram:: IPython.lib.pretty :parts: 3 :copyright: 2007 by Armin Ronacher. Portions (c) 2009 by Robert Kern. :license: BSD License. """ import ast import datetime import re import struct import sys import types import warnings from collections import Counter, OrderedDict, defaultdict, deque from collections.abc import Callable, Generator, Iterable, Sequence from contextlib import contextmanager, suppress from enum import Enum, Flag from functools import partial from io import StringIO, TextIOBase from math import copysign, isnan from typing import TYPE_CHECKING, Any, Optional, TypeAlias, TypeVar if TYPE_CHECKING: from hypothesis.control import BuildContext T = TypeVar("T") PrettyPrintFunction: TypeAlias = Callable[[Any, "RepresentationPrinter", bool], None] ArgLabelsT: TypeAlias = dict[str, tuple[int, int]] __all__ = [ "IDKey", "RepresentationPrinter", "_fixeddict_pprinter", "_tuple_pprinter", "pretty", ] def _safe_getattr(obj: object, attr: str, default: Any | None = None) -> Any: """Safe version of getattr. Same as getattr, but will return ``default`` on any Exception, rather than raising. """ try: return getattr(obj, attr, default) except Exception: return default def pretty(obj: object, *, cycle: bool = False) -> str: """Pretty print the object's representation.""" printer = RepresentationPrinter() printer.pretty(obj, cycle=cycle) return printer.getvalue() class IDKey: def __init__(self, value: object): self.value = value def __hash__(self) -> int: return hash((type(self), id(self.value))) def __eq__(self, __o: object) -> bool: return isinstance(__o, type(self)) and id(self.value) == id(__o.value) class RepresentationPrinter: """Special pretty printer that has a `pretty` method that calls the pretty printer for a python object. This class stores processing data on `self` so you must *never* use this class in a threaded environment. Always lock it or reinstantiate it. """ def __init__( self, output: TextIOBase | None = None, *, context: Optional["BuildContext"] = None, ) -> None: """Optionally pass the output stream and the current build context. We use the context to represent objects constructed by strategies by showing *how* they were constructed, and add annotations showing which parts of the minimal failing example can vary without changing the test result. """ self.broken: bool = False self.output: TextIOBase = StringIO() if output is None else output self.max_width: int = 79 self.max_seq_length: int = 1000 self.output_width: int = 0 self.buffer_width: int = 0 self.buffer: deque[Breakable | Text] = deque() root_group = Group(0) self.group_stack = [root_group] self.group_queue = GroupQueue(root_group) self.indentation: int = 0 self.stack: list[int] = [] self.singleton_pprinters: dict[int, PrettyPrintFunction] = {} self.type_pprinters: dict[type, PrettyPrintFunction] = {} self.deferred_pprinters: dict[tuple[str, str], PrettyPrintFunction] = {} # If IPython has been imported, load up their pretty-printer registry if "IPython.lib.pretty" in sys.modules: ipp = sys.modules["IPython.lib.pretty"] self.singleton_pprinters.update(ipp._singleton_pprinters) self.type_pprinters.update(ipp._type_pprinters) self.deferred_pprinters.update(ipp._deferred_type_pprinters) # If there's overlap between our pprinters and IPython's, we'll use ours. self.singleton_pprinters.update(_singleton_pprinters) self.type_pprinters.update(_type_pprinters) self.deferred_pprinters.update(_deferred_type_pprinters) # for which-parts-matter, we track a mapping from the (start_idx, end_idx) # of slices into the minimal failing example; this is per-interesting_origin # but we report each separately so that's someone else's problem here. # Invocations of self.repr_call() can report the slice for each argument, # which will then be used to look up the relevant comment if any. self.known_object_printers: dict[IDKey, list[PrettyPrintFunction]] self.slice_comments: dict[tuple[int, int], str] if context is None: self.known_object_printers = defaultdict(list) self.slice_comments = {} else: self.known_object_printers = context.known_object_printers self.slice_comments = context.data.slice_comments assert all(isinstance(k, IDKey) for k in self.known_object_printers) # Track which slices we've already printed comments for, to avoid # duplicating comments when nested objects share the same slice range. self._commented_slices: set[tuple[int, int]] = set() def pretty(self, obj: object, *, cycle: bool = False) -> None: """Pretty print the given object.""" obj_id = id(obj) cycle = cycle or obj_id in self.stack self.stack.append(obj_id) try: with self.group(): obj_class = _safe_getattr(obj, "__class__", None) or type(obj) # First try to find registered singleton printers for the type. try: printer = self.singleton_pprinters[obj_id] except (TypeError, KeyError): pass else: return printer(obj, self, cycle) # Look for the _repr_pretty_ method which allows users # to define custom pretty printing. # Some objects automatically create any requested # attribute. Try to ignore most of them by checking for # callability. pretty_method = _safe_getattr(obj, "_repr_pretty_", None) if callable(pretty_method): return pretty_method(self, cycle) # Check for object-specific printers which show how this # object was constructed (a Hypothesis special feature). # This must come before type_pprinters so that sub-argument # comments are shown for tuples/dicts/etc. printers = self.known_object_printers[IDKey(obj)] if len(printers) == 1: return printers[0](obj, self, cycle) if printers: # Multiple registered functions for the same object (due to # caching, small ints, etc). Use the first if all produce # the same string; otherwise pretend none were registered. strs = set() for f in printers: p = RepresentationPrinter() f(obj, p, cycle) strs.add(p.getvalue()) if len(strs) == 1: return printers[0](obj, self, cycle) # Next walk the mro and check for either: # 1) a registered printer # 2) a _repr_pretty_ method for cls in obj_class.__mro__: if cls in self.type_pprinters: # printer registered in self.type_pprinters return self.type_pprinters[cls](obj, self, cycle) else: # Check if the given class is specified in the deferred type # registry; move it to the regular type registry if so. key = ( _safe_getattr(cls, "__module__", None), _safe_getattr(cls, "__name__", None), ) if key in self.deferred_pprinters: # Move the printer over to the regular registry. printer = self.deferred_pprinters.pop(key) self.type_pprinters[cls] = printer return printer(obj, self, cycle) else: if hasattr(cls, "__attrs_attrs__"): # pragma: no cover return pprint_fields( obj, self, cycle, [at.name for at in cls.__attrs_attrs__ if at.init], ) if hasattr(cls, "__dataclass_fields__"): return pprint_fields( obj, self, cycle, [ k for k, v in cls.__dataclass_fields__.items() if v.init ], ) # A user-provided repr. Find newlines and replace them with p.break_() return _repr_pprint(obj, self, cycle) finally: self.stack.pop() def _break_outer_groups(self) -> None: while self.max_width < self.output_width + self.buffer_width: group = self.group_queue.deq() if not group: return while group.breakables: x = self.buffer.popleft() self.output_width = x.output(self.output, self.output_width) self.buffer_width -= x.width while self.buffer and isinstance(self.buffer[0], Text): x = self.buffer.popleft() self.output_width = x.output(self.output, self.output_width) self.buffer_width -= x.width def text(self, obj: str) -> None: """Add literal text to the output.""" width = len(obj) if self.buffer: text = self.buffer[-1] if not isinstance(text, Text): text = Text() self.buffer.append(text) text.add(obj, width) self.buffer_width += width self._break_outer_groups() else: self.output.write(obj) self.output_width += width def breakable(self, sep: str = " ") -> None: """Add a breakable separator to the output. This does not mean that it will automatically break here. If no breaking on this position takes place the `sep` is inserted which default to one space. """ width = len(sep) group = self.group_stack[-1] if group.want_break: self.flush() self.output.write("\n" + " " * self.indentation) self.output_width = self.indentation self.buffer_width = 0 else: self.buffer.append(Breakable(sep, width, self)) self.buffer_width += width self._break_outer_groups() def break_(self) -> None: """Explicitly insert a newline into the output, maintaining correct indentation.""" self.flush() self.output.write("\n" + " " * self.indentation) self.output_width = self.indentation self.buffer_width = 0 @contextmanager def indent(self, indent: int) -> Generator[None, None, None]: """`with`-statement support for indenting/dedenting.""" self.indentation += indent try: yield finally: self.indentation -= indent @contextmanager def group( self, indent: int = 0, open: str = "", close: str = "" ) -> Generator[None, None, None]: """Context manager for an indented group. with p.group(1, '{', '}'): The first parameter specifies the indentation for the next line (usually the width of the opening text), the second and third the opening and closing delimiters. """ self.begin_group(indent=indent, open=open) try: yield finally: self.end_group(dedent=indent, close=close) def begin_group(self, indent: int = 0, open: str = "") -> None: """Use the `with group(...) context manager instead. The begin_group() and end_group() methods are for IPython compatibility only; see https://github.com/HypothesisWorks/hypothesis/issues/3721 for details. """ if open: self.text(open) group = Group(self.group_stack[-1].depth + 1) self.group_stack.append(group) self.group_queue.enq(group) self.indentation += indent def end_group(self, dedent: int = 0, close: str = "") -> None: """See begin_group().""" self.indentation -= dedent group = self.group_stack.pop() if not group.breakables: self.group_queue.remove(group) if close: self.text(close) def _enumerate(self, seq: Iterable[T]) -> Generator[tuple[int, T], None, None]: """Like enumerate, but with an upper limit on the number of items.""" for idx, x in enumerate(seq): if self.max_seq_length and idx >= self.max_seq_length: self.text(",") self.breakable() self.text("...") return yield idx, x def flush(self) -> None: """Flush data that is left in the buffer.""" for data in self.buffer: self.output_width += data.output(self.output, self.output_width) self.buffer.clear() self.buffer_width = 0 def getvalue(self) -> str: assert isinstance(self.output, StringIO) self.flush() return self.output.getvalue() def maybe_repr_known_object_as_call( self, obj: object, cycle: bool, name: str, args: Sequence[object], kwargs: dict[str, object], arg_labels: ArgLabelsT | None = None, ) -> None: # pprint this object as a call, _unless_ the call would be invalid syntax # and the repr would be valid and there are not comments on arguments. if cycle: return self.text("<...>") # Look up comments from slice_comments if we have arg_labels comments = {} if arg_labels is not None: for key, sr in arg_labels.items(): if sr in self.slice_comments: comments[key] = self.slice_comments[sr] # If there are comments, we must use our call-style repr regardless of syntax if not comments: with suppress(Exception): # Check whether the repr is valid syntax: ast.parse(repr(obj)) # Given that the repr is valid syntax, check the call: p = RepresentationPrinter() p.stack = self.stack.copy() p.known_object_printers = self.known_object_printers p.repr_call(name, args, kwargs) # If the call is not valid syntax, use the repr try: ast.parse(p.getvalue()) except Exception: return _repr_pprint(obj, self, cycle) return self.repr_call(name, args, kwargs, arg_slices=arg_labels) def repr_call( self, func_name: str, args: Sequence[object], kwargs: dict[str, object], *, force_split: bool | None = None, arg_slices: ArgLabelsT | None = None, leading_comment: str | None = None, avoid_realization: bool = False, ) -> None: """Helper function to represent a function call. - func_name, args, and kwargs should all be pretty obvious. - If split_lines, we'll force one-argument-per-line; otherwise we'll place calls that fit on a single line (and split otherwise). - arg_slices is a mapping from pos-idx or keyword to (start_idx, end_idx) of the Conjecture buffer, by which we can look up comments to add. """ assert isinstance(func_name, str) if func_name.startswith(("lambda:", "lambda ")): func_name = f"({func_name})" self.text(func_name) # Build list of (label, value) pairs. Labels are "arg[i]" for positional # args, or the keyword name. Skip slices already commented at a higher level. all_args = [(f"arg[{i}]", v) for i, v in enumerate(args)] all_args += list(kwargs.items()) arg_slices = arg_slices or {} comments: dict[str, tuple[str, tuple[int, int]]] = {} for label, sr in arg_slices.items(): if sr in self.slice_comments and sr not in self._commented_slices: comments[label] = (self.slice_comments[sr], sr) if leading_comment or any(k in comments for k, _ in all_args): # We have to split one arg per line in order to leave comments on them. force_split = True if force_split is None: # We're OK with printing this call on a single line, but will it fit? # If not, we'd rather fall back to one-argument-per-line instead. p = RepresentationPrinter() p.stack = self.stack.copy() p.known_object_printers = self.known_object_printers p.repr_call("_" * self.output_width, args, kwargs, force_split=False) s = p.getvalue() force_split = "\n" in s with self.group(indent=4, open="(", close=""): for i, (label, v) in enumerate(all_args): if force_split: if i == 0 and leading_comment: self.break_() self.text(leading_comment) self.break_() else: assert leading_comment is None # only passed by top-level report self.breakable(" " if i else "") if not label.startswith("arg["): self.text(f"{label}=") # Mark slice as commented BEFORE printing value, so nested printers skip it entry = comments.get(label) if entry: self._commented_slices.add(entry[1]) if avoid_realization: self.text("") else: self.pretty(v) if force_split or i + 1 < len(all_args): self.text(",") if entry: self.text(f" # {entry[0]}") if all_args and force_split: self.break_() self.text(")") # after dedent class Printable: def output(self, stream: TextIOBase, output_width: int) -> int: # pragma: no cover raise NotImplementedError class Text(Printable): def __init__(self) -> None: self.objs: list[str] = [] self.width: int = 0 def output(self, stream: TextIOBase, output_width: int) -> int: for obj in self.objs: stream.write(obj) return output_width + self.width def add(self, obj: str, width: int) -> None: self.objs.append(obj) self.width += width class Breakable(Printable): def __init__(self, seq: str, width: int, pretty: RepresentationPrinter) -> None: self.obj = seq self.width = width self.pretty = pretty self.indentation = pretty.indentation self.group = pretty.group_stack[-1] self.group.breakables.append(self) def output(self, stream: TextIOBase, output_width: int) -> int: self.group.breakables.popleft() if self.group.want_break: stream.write("\n" + " " * self.indentation) return self.indentation if not self.group.breakables: self.pretty.group_queue.remove(self.group) stream.write(self.obj) return output_width + self.width class Group(Printable): def __init__(self, depth: int) -> None: self.depth = depth self.breakables: deque[Breakable] = deque() self.want_break: bool = False class GroupQueue: def __init__(self, *groups: Group) -> None: self.queue: list[list[Group]] = [] for group in groups: self.enq(group) def enq(self, group: Group) -> None: depth = group.depth while depth > len(self.queue) - 1: self.queue.append([]) self.queue[depth].append(group) def deq(self) -> Group | None: for stack in self.queue: for idx, group in enumerate(reversed(stack)): if group.breakables: del stack[idx] group.want_break = True return group for group in stack: group.want_break = True del stack[:] return None def remove(self, group: Group) -> None: try: self.queue[group.depth].remove(group) except ValueError: pass def _seq_pprinter_factory(start: str, end: str, basetype: type) -> PrettyPrintFunction: """Factory that returns a pprint function useful for sequences. Used by the default pprint for tuples, dicts, and lists. """ def inner( obj: tuple[object] | list[object], p: RepresentationPrinter, cycle: bool ) -> None: typ = type(obj) if ( basetype is not None and typ is not basetype and typ.__repr__ != basetype.__repr__ # type: ignore[comparison-overlap] ): # If the subclass provides its own repr, use it instead. return p.text(typ.__repr__(obj)) if cycle: return p.text(start + "..." + end) step = len(start) with p.group(step, start, end): for idx, x in p._enumerate(obj): if idx: p.text(",") p.breakable() p.pretty(x) if len(obj) == 1 and type(obj) is tuple: # Special case for 1-item tuples. p.text(",") return inner def get_class_name(cls: type[object]) -> str: class_name = _safe_getattr(cls, "__qualname__", cls.__name__) assert isinstance(class_name, str) return class_name def _set_pprinter_factory( start: str, end: str, basetype: type[object] ) -> PrettyPrintFunction: """Factory that returns a pprint function useful for sets and frozensets.""" def inner( obj: set[Any] | frozenset[Any], p: RepresentationPrinter, cycle: bool, ) -> None: typ = type(obj) if ( basetype is not None and typ is not basetype and typ.__repr__ != basetype.__repr__ ): # If the subclass provides its own repr, use it instead. return p.text(typ.__repr__(obj)) if cycle: return p.text(start + "..." + end) if not obj: # Special case. p.text(get_class_name(basetype) + "()") else: step = len(start) with p.group(step, start, end): # Like dictionary keys, try to sort the items if there aren't too many items: Iterable[object] = obj if not (p.max_seq_length and len(obj) >= p.max_seq_length): try: items = sorted(obj) except Exception: # Sometimes the items don't sort. pass for idx, x in p._enumerate(items): if idx: p.text(",") p.breakable() p.pretty(x) return inner def _dict_pprinter_factory( start: str, end: str, basetype: type[object] | None = None ) -> PrettyPrintFunction: """Factory that returns a pprint function used by the default pprint of dicts and dict proxies.""" def inner(obj: dict[object, object], p: RepresentationPrinter, cycle: bool) -> None: typ = type(obj) if ( basetype is not None and typ is not basetype and typ.__repr__ != basetype.__repr__ ): # If the subclass provides its own repr, use it instead. return p.text(typ.__repr__(obj)) if cycle: return p.text("{...}") with ( p.group(1, start, end), # If the dict contains both "" and b"" (empty string and empty bytes), we # ignore the BytesWarning raised by `python -bb` mode. We can't use # `.items()` because it might be a non-`dict` type of mapping. warnings.catch_warnings(), ): warnings.simplefilter("ignore", BytesWarning) for idx, key in p._enumerate(obj): if idx: p.text(",") p.breakable() p.pretty(key) p.text(": ") p.pretty(obj[key]) inner.__name__ = f"_dict_pprinter_factory({start!r}, {end!r}, {basetype!r})" return inner def _super_pprint(obj: Any, p: RepresentationPrinter, cycle: bool) -> None: """The pprint for the super type.""" with p.group(8, ""): p.pretty(obj.__thisclass__) p.text(",") p.breakable() p.pretty(obj.__self__) def _re_pattern_pprint(obj: re.Pattern, p: RepresentationPrinter, cycle: bool) -> None: """The pprint function for regular expression patterns.""" p.text("re.compile(") pattern = repr(obj.pattern) if pattern[:1] in "uU": # pragma: no cover pattern = pattern[1:] prefix = "ur" else: prefix = "r" pattern = prefix + pattern.replace("\\\\", "\\") p.text(pattern) if obj.flags: p.text(",") p.breakable() done_one = False for flag in ( "TEMPLATE", "IGNORECASE", "LOCALE", "MULTILINE", "DOTALL", "UNICODE", "VERBOSE", "DEBUG", ): if obj.flags & getattr(re, flag, 0): if done_one: p.text("|") p.text("re." + flag) done_one = True p.text(")") def _type_pprint(obj: type[object], p: RepresentationPrinter, cycle: bool) -> None: """The pprint for classes and types.""" # Heap allocated types might not have the module attribute, # and others may set it to None. # Checks for a __repr__ override in the metaclass # != rather than is not because pypy compatibility if type(obj).__repr__ != type.__repr__: # type: ignore[comparison-overlap] _repr_pprint(obj, p, cycle) return mod = _safe_getattr(obj, "__module__", None) try: name = obj.__qualname__ except Exception: # pragma: no cover name = obj.__name__ if not isinstance(name, str): name = "" if mod in (None, "__builtin__", "builtins", "exceptions"): p.text(name) else: p.text(mod + "." + name) def _repr_pprint(obj: object, p: RepresentationPrinter, cycle: bool) -> None: """A pprint that just redirects to the normal repr function.""" # Find newlines and replace them with p.break_() output = repr(obj) for idx, output_line in enumerate(output.splitlines()): if idx: p.break_() p.text(output_line) def pprint_fields( obj: object, p: RepresentationPrinter, cycle: bool, fields: Iterable[str] ) -> None: name = get_class_name(obj.__class__) if cycle: return p.text(f"{name}(...)") with p.group(1, name + "(", ")"): for idx, field in enumerate(fields): if idx: p.text(",") p.breakable() p.text(field) p.text("=") p.pretty(getattr(obj, field)) def _get_slice_comment( p: RepresentationPrinter, arg_labels: ArgLabelsT, key: Any, ) -> tuple[str, tuple[int, int]] | None: """Look up a comment for a slice, if not already printed at a higher level.""" if (sr := arg_labels.get(key)) and sr in p.slice_comments: if sr not in p._commented_slices: return (p.slice_comments[sr], sr) return None def _tuple_pprinter(arg_labels: ArgLabelsT) -> PrettyPrintFunction: """Pretty printer for tuples that shows sub-argument comments.""" def inner(obj: tuple, p: RepresentationPrinter, cycle: bool) -> None: if cycle: return p.text("(...)") get = lambda i: _get_slice_comment(p, arg_labels, f"arg[{i}]") has_comments = any(get(i) for i in range(len(obj))) with p.group(indent=4, open="(", close=""): for idx, x in p._enumerate(obj): p.break_() if has_comments else (p.breakable() if idx else None) p.pretty(x) if has_comments or idx + 1 < len(obj) or len(obj) == 1: p.text(",") if entry := get(idx): p._commented_slices.add(entry[1]) p.text(f" # {entry[0]}") if has_comments and obj: p.break_() p.text(")") return inner def _fixeddict_pprinter( arg_labels: ArgLabelsT, mapping: dict[Any, Any], ) -> PrettyPrintFunction: """Pretty printer for fixed_dictionaries that shows sub-argument comments.""" def inner(obj: dict, p: RepresentationPrinter, cycle: bool) -> None: if cycle: return p.text("{...}") get = lambda k: _get_slice_comment(p, arg_labels, k) # Preserve mapping key order, then any optional keys (deduped) keys = list(dict.fromkeys(k for k in [*mapping, *obj] if k in obj)) has_comments = any(get(k) for k in keys) with p.group(indent=4, open="{", close=""): for idx, key in p._enumerate(keys): p.break_() if has_comments else (p.breakable() if idx else None) p.pretty(key) p.text(": ") p.pretty(obj[key]) if has_comments or idx + 1 < len(keys): p.text(",") if entry := get(key): p._commented_slices.add(entry[1]) p.text(f" # {entry[0]}") if has_comments and obj: p.break_() p.text("}") return inner def _function_pprint( obj: types.FunctionType | types.BuiltinFunctionType | types.MethodType, p: RepresentationPrinter, cycle: bool, ) -> None: """Base pprint for all functions and builtin functions.""" from hypothesis.internal.reflection import get_pretty_function_description p.text(get_pretty_function_description(obj)) def _exception_pprint( obj: BaseException, p: RepresentationPrinter, cycle: bool ) -> None: """Base pprint for all exceptions.""" name = getattr(obj.__class__, "__qualname__", obj.__class__.__name__) if obj.__class__.__module__ not in ("exceptions", "builtins"): name = f"{obj.__class__.__module__}.{name}" step = len(name) + 1 with p.group(step, name + "(", ")"): for idx, arg in enumerate(getattr(obj, "args", ())): if idx: p.text(",") p.breakable() p.pretty(arg) def _repr_integer(obj: int, p: RepresentationPrinter, cycle: bool) -> None: if abs(obj) < 1_000_000_000: p.text(repr(obj)) elif abs(obj) < 10**640: # add underscores for integers over ten decimal digits p.text(f"{obj:#_d}") else: # for very very large integers, use hex because power-of-two bases are cheaper # https://docs.python.org/3/library/stdtypes.html#integer-string-conversion-length-limitation p.text(f"{obj:#_x}") def _repr_float_counting_nans( obj: float, p: RepresentationPrinter, cycle: bool ) -> None: if isnan(obj): if struct.pack("!d", abs(obj)) != struct.pack("!d", float("nan")): show = hex(*struct.unpack("Q", struct.pack("d", obj))) return p.text(f"struct.unpack('d', struct.pack('Q', {show}))[0]") elif copysign(1.0, obj) == -1.0: return p.text("-nan") p.text(repr(obj)) #: printers for builtin types _type_pprinters: dict[type, PrettyPrintFunction] = { int: _repr_integer, float: _repr_float_counting_nans, str: _repr_pprint, tuple: _seq_pprinter_factory("(", ")", tuple), list: _seq_pprinter_factory("[", "]", list), dict: _dict_pprinter_factory("{", "}", dict), set: _set_pprinter_factory("{", "}", set), frozenset: _set_pprinter_factory("frozenset({", "})", frozenset), super: _super_pprint, re.Pattern: _re_pattern_pprint, type: _type_pprint, types.FunctionType: _function_pprint, types.BuiltinFunctionType: _function_pprint, types.MethodType: _function_pprint, datetime.datetime: _repr_pprint, datetime.timedelta: _repr_pprint, BaseException: _exception_pprint, slice: _repr_pprint, range: _repr_pprint, bytes: _repr_pprint, } #: printers for types specified by name _deferred_type_pprinters: dict[tuple[str, str], PrettyPrintFunction] = {} def for_type_by_name( type_module: str, type_name: str, func: PrettyPrintFunction ) -> PrettyPrintFunction | None: """Add a pretty printer for a type specified by the module and name of a type rather than the type object itself.""" key = (type_module, type_name) oldfunc = _deferred_type_pprinters.get(key) _deferred_type_pprinters[key] = func return oldfunc #: printers for the default singletons _singleton_pprinters: dict[int, PrettyPrintFunction] = dict.fromkeys( map(id, [None, True, False, Ellipsis, NotImplemented]), _repr_pprint ) def _defaultdict_pprint( obj: defaultdict[object, object], p: RepresentationPrinter, cycle: bool ) -> None: name = obj.__class__.__name__ with p.group(len(name) + 1, name + "(", ")"): if cycle: p.text("...") else: p.pretty(obj.default_factory) p.text(",") p.breakable() p.pretty(dict(obj)) def _ordereddict_pprint( obj: OrderedDict[object, object], p: RepresentationPrinter, cycle: bool ) -> None: name = obj.__class__.__name__ with p.group(len(name) + 1, name + "(", ")"): if cycle: p.text("...") elif obj: p.pretty(list(obj.items())) def _deque_pprint(obj: deque[object], p: RepresentationPrinter, cycle: bool) -> None: name = obj.__class__.__name__ with p.group(len(name) + 1, name + "(", ")"): if cycle: p.text("...") else: p.pretty(list(obj)) def _counter_pprint( obj: Counter[object], p: RepresentationPrinter, cycle: bool ) -> None: name = obj.__class__.__name__ with p.group(len(name) + 1, name + "(", ")"): if cycle: p.text("...") elif obj: p.pretty(dict(obj)) def _repr_dataframe( obj: object, p: RepresentationPrinter, cycle: bool ) -> None: # pragma: no cover with p.indent(4): p.break_() _repr_pprint(obj, p, cycle) p.break_() def _repr_enum(obj: Enum, p: RepresentationPrinter, cycle: bool) -> None: tname = get_class_name(type(obj)) if isinstance(obj, Flag): p.text( " | ".join(f"{tname}.{x.name}" for x in type(obj) if x & obj == x) or f"{tname}({obj.value!r})" # if no matching members ) else: p.text(f"{tname}.{obj.name}") class _ReprDots: def __repr__(self) -> str: return "..." def _repr_partial(obj: partial[Any], p: RepresentationPrinter, cycle: bool) -> None: args, kw = obj.args, obj.keywords if cycle: args, kw = (_ReprDots(),), {} p.repr_call(pretty(type(obj)), (obj.func, *args), kw) for_type_by_name("collections", "defaultdict", _defaultdict_pprint) for_type_by_name("collections", "OrderedDict", _ordereddict_pprint) for_type_by_name("ordereddict", "OrderedDict", _ordereddict_pprint) for_type_by_name("collections", "deque", _deque_pprint) for_type_by_name("collections", "Counter", _counter_pprint) for_type_by_name("pandas.core.frame", "DataFrame", _repr_dataframe) for_type_by_name("enum", "Enum", _repr_enum) for_type_by_name("functools", "partial", _repr_partial) ================================================ FILE: hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt ================================================ # Version 2026021400, Last Updated Sat Feb 14 07:07:01 2026 UTC AAA AARP ABB ABBOTT ABBVIE ABC ABLE ABOGADO ABUDHABI AC ACADEMY ACCENTURE ACCOUNTANT ACCOUNTANTS ACO ACTOR AD ADS ADULT AE AEG AERO AETNA AF AFL AFRICA AG AGAKHAN AGENCY AI AIG AIRBUS AIRFORCE AIRTEL AKDN AL ALIBABA ALIPAY ALLFINANZ ALLSTATE ALLY ALSACE ALSTOM AM AMAZON AMERICANEXPRESS AMERICANFAMILY AMEX AMFAM AMICA AMSTERDAM ANALYTICS ANDROID ANQUAN ANZ AO AOL APARTMENTS APP APPLE AQ AQUARELLE AR ARAB ARAMCO ARCHI ARMY ARPA ART ARTE AS ASDA ASIA ASSOCIATES AT ATHLETA ATTORNEY AU AUCTION AUDI AUDIBLE AUDIO AUSPOST AUTHOR AUTO AUTOS AW AWS AX AXA AZ AZURE BA BABY BAIDU BANAMEX BAND BANK BAR BARCELONA BARCLAYCARD BARCLAYS BAREFOOT BARGAINS BASEBALL BASKETBALL BAUHAUS BAYERN BB BBC BBT BBVA BCG BCN BD BE BEATS BEAUTY BEER BERLIN BEST BESTBUY BET BF BG BH BHARTI BI BIBLE BID BIKE BING BINGO BIO BIZ BJ BLACK BLACKFRIDAY BLOCKBUSTER BLOG BLOOMBERG BLUE BM BMS BMW BN BNPPARIBAS BO BOATS BOEHRINGER BOFA BOM BOND BOO BOOK BOOKING BOSCH BOSTIK BOSTON BOT BOUTIQUE BOX BR BRADESCO BRIDGESTONE BROADWAY BROKER BROTHER BRUSSELS BS BT BUILD BUILDERS BUSINESS BUY BUZZ BV BW BY BZ BZH CA CAB CAFE CAL CALL CALVINKLEIN CAM CAMERA CAMP CANON CAPETOWN CAPITAL CAPITALONE CAR CARAVAN CARDS CARE CAREER CAREERS CARS CASA CASE CASH CASINO CAT CATERING CATHOLIC CBA CBN CBRE CC CD CENTER CEO CERN CF CFA CFD CG CH CHANEL CHANNEL CHARITY CHASE CHAT CHEAP CHINTAI CHRISTMAS CHROME CHURCH CI CIPRIANI CIRCLE CISCO CITADEL CITI CITIC CITY CK CL CLAIMS CLEANING CLICK CLINIC CLINIQUE CLOTHING CLOUD CLUB CLUBMED CM CN CO COACH CODES COFFEE COLLEGE COLOGNE COM COMMBANK COMMUNITY COMPANY COMPARE COMPUTER COMSEC CONDOS CONSTRUCTION CONSULTING CONTACT CONTRACTORS COOKING COOL COOP CORSICA COUNTRY COUPON COUPONS COURSES CPA CR CREDIT CREDITCARD CREDITUNION CRICKET CROWN CRS CRUISE CRUISES CU CUISINELLA CV CW CX CY CYMRU CYOU CZ DAD DANCE DATA DATE DATING DATSUN DAY DCLK DDS DE DEAL DEALER DEALS DEGREE DELIVERY DELL DELOITTE DELTA DEMOCRAT DENTAL DENTIST DESI DESIGN DEV DHL DIAMONDS DIET DIGITAL DIRECT DIRECTORY DISCOUNT DISCOVER DISH DIY DJ DK DM DNP DO DOCS DOCTOR DOG DOMAINS DOT DOWNLOAD DRIVE DTV DUBAI DUPONT DURBAN DVAG DVR DZ EARTH EAT EC ECO EDEKA EDU EDUCATION EE EG EMAIL EMERCK ENERGY ENGINEER ENGINEERING ENTERPRISES EPSON EQUIPMENT ER ERICSSON ERNI ES ESQ ESTATE ET EU EUROVISION EUS EVENTS EXCHANGE EXPERT EXPOSED EXPRESS EXTRASPACE FAGE FAIL FAIRWINDS FAITH FAMILY FAN FANS FARM FARMERS FASHION FAST FEDEX FEEDBACK FERRARI FERRERO FI FIDELITY FIDO FILM FINAL FINANCE FINANCIAL FIRE FIRESTONE FIRMDALE FISH FISHING FIT FITNESS FJ FK FLICKR FLIGHTS FLIR FLORIST FLOWERS FLY FM FO FOO FOOD FOOTBALL FORD FOREX FORSALE FORUM FOUNDATION FOX FR FREE FRESENIUS FRL FROGANS FRONTIER FTR FUJITSU FUN FUND FURNITURE FUTBOL FYI GA GAL GALLERY GALLO GALLUP GAME GAMES GAP GARDEN GAY GB GBIZ GD GDN GE GEA GENT GENTING GEORGE GF GG GGEE GH GI GIFT GIFTS GIVES GIVING GL GLASS GLE GLOBAL GLOBO GM GMAIL GMBH GMO GMX GN GODADDY GOLD GOLDPOINT GOLF GOODYEAR GOOG GOOGLE GOP GOT GOV GP GQ GR GRAINGER GRAPHICS GRATIS GREEN GRIPE GROCERY GROUP GS GT GU GUCCI GUGE GUIDE GUITARS GURU GW GY HAIR HAMBURG HANGOUT HAUS HBO HDFC HDFCBANK HEALTH HEALTHCARE HELP HELSINKI HERE HERMES HIPHOP HISAMITSU HITACHI HIV HK HKT HM HN HOCKEY HOLDINGS HOLIDAY HOMEDEPOT HOMEGOODS HOMES HOMESENSE HONDA HORSE HOSPITAL HOST HOSTING HOT HOTELS HOTMAIL HOUSE HOW HR HSBC HT HU HUGHES HYATT HYUNDAI IBM ICBC ICE ICU ID IE IEEE IFM IKANO IL IM IMAMAT IMDB IMMO IMMOBILIEN IN INC INDUSTRIES INFINITI INFO ING INK INSTITUTE INSURANCE INSURE INT INTERNATIONAL INTUIT INVESTMENTS IO IPIRANGA IQ IR IRISH IS ISMAILI IST ISTANBUL IT ITAU ITV JAGUAR JAVA JCB JE JEEP JETZT JEWELRY JIO JLL JM JMP JNJ JO JOBS JOBURG JOT JOY JP JPMORGAN JPRS JUEGOS JUNIPER KAUFEN KDDI KE KERRYHOTELS KERRYPROPERTIES KFH KG KH KI KIA KIDS KIM KINDLE KITCHEN KIWI KM KN KOELN KOMATSU KOSHER KP KPMG KPN KR KRD KRED KUOKGROUP KW KY KYOTO KZ LA LACAIXA LAMBORGHINI LAMER LAND LANDROVER LANXESS LASALLE LAT LATINO LATROBE LAW LAWYER LB LC LDS LEASE LECLERC LEFRAK LEGAL LEGO LEXUS LGBT LI LIDL LIFE LIFEINSURANCE LIFESTYLE LIGHTING LIKE LILLY LIMITED LIMO LINCOLN LINK LIVE LIVING LK LLC LLP LOAN LOANS LOCKER LOCUS LOL LONDON LOTTE LOTTO LOVE LPL LPLFINANCIAL LR LS LT LTD LTDA LU LUNDBECK LUXE LUXURY LV LY MA MADRID MAIF MAISON MAKEUP MAN MANAGEMENT MANGO MAP MARKET MARKETING MARKETS MARRIOTT MARSHALLS MATTEL MBA MC MCKINSEY MD ME MED MEDIA MEET MELBOURNE MEME MEMORIAL MEN MENU MERCKMSD MG MH MIAMI MICROSOFT MIL MINI MINT MIT MITSUBISHI MK ML MLB MLS MM MMA MN MO MOBI MOBILE MODA MOE MOI MOM MONASH MONEY MONSTER MORMON MORTGAGE MOSCOW MOTO MOTORCYCLES MOV MOVIE MP MQ MR MS MSD MT MTN MTR MU MUSEUM MUSIC MV MW MX MY MZ NA NAB NAGOYA NAME NAVY NBA NC NE NEC NET NETBANK NETFLIX NETWORK NEUSTAR NEW NEWS NEXT NEXTDIRECT NEXUS NF NFL NG NGO NHK NI NICO NIKE NIKON NINJA NISSAN NISSAY NL NO NOKIA NORTON NOW NOWRUZ NOWTV NP NR NRA NRW NTT NU NYC NZ OBI OBSERVER OFFICE OKINAWA OLAYAN OLAYANGROUP OLLO OM OMEGA ONE ONG ONL ONLINE OOO OPEN ORACLE ORANGE ORG ORGANIC ORIGINS OSAKA OTSUKA OTT OVH PA PAGE PANASONIC PARIS PARS PARTNERS PARTS PARTY PAY PCCW PE PET PF PFIZER PG PH PHARMACY PHD PHILIPS PHONE PHOTO PHOTOGRAPHY PHOTOS PHYSIO PICS PICTET PICTURES PID PIN PING PINK PIONEER PIZZA PK PL PLACE PLAY PLAYSTATION PLUMBING PLUS PM PN PNC POHL POKER POLITIE PORN POST PR PRAXI PRESS PRIME PRO PROD PRODUCTIONS PROF PROGRESSIVE PROMO PROPERTIES PROPERTY PROTECTION PRU PRUDENTIAL PS PT PUB PW PWC PY QA QPON QUEBEC QUEST RACING RADIO RE READ REALESTATE REALTOR REALTY RECIPES RED REDUMBRELLA REHAB REISE REISEN REIT RELIANCE REN RENT RENTALS REPAIR REPORT REPUBLICAN REST RESTAURANT REVIEW REVIEWS REXROTH RICH RICHARDLI RICOH RIL RIO RIP RO ROCKS RODEO ROGERS ROOM RS RSVP RU RUGBY RUHR RUN RW RWE RYUKYU SA SAARLAND SAFE SAFETY SAKURA SALE SALON SAMSCLUB SAMSUNG SANDVIK SANDVIKCOROMANT SANOFI SAP SARL SAS SAVE SAXO SB SBI SBS SC SCB SCHAEFFLER SCHMIDT SCHOLARSHIPS SCHOOL SCHULE SCHWARZ SCIENCE SCOT SD SE SEARCH SEAT SECURE SECURITY SEEK SELECT SENER SERVICES SEVEN SEW SEX SEXY SFR SG SH SHANGRILA SHARP SHELL SHIA SHIKSHA SHOES SHOP SHOPPING SHOUJI SHOW SI SILK SINA SINGLES SITE SJ SK SKI SKIN SKY SKYPE SL SLING SM SMART SMILE SN SNCF SO SOCCER SOCIAL SOFTBANK SOFTWARE SOHU SOLAR SOLUTIONS SONG SONY SOY SPA SPACE SPORT SPOT SR SRL SS ST STADA STAPLES STAR STATEBANK STATEFARM STC STCGROUP STOCKHOLM STORAGE STORE STREAM STUDIO STUDY STYLE SU SUCKS SUPPLIES SUPPLY SUPPORT SURF SURGERY SUZUKI SV SWATCH SWISS SX SY SYDNEY SYSTEMS SZ TAB TAIPEI TALK TAOBAO TARGET TATAMOTORS TATAR TATTOO TAX TAXI TC TCI TD TDK TEAM TECH TECHNOLOGY TEL TEMASEK TENNIS TEVA TF TG TH THD THEATER THEATRE TIAA TICKETS TIENDA TIPS TIRES TIROL TJ TJMAXX TJX TK TKMAXX TL TM TMALL TN TO TODAY TOKYO TOOLS TOP TORAY TOSHIBA TOTAL TOURS TOWN TOYOTA TOYS TR TRADE TRADING TRAINING TRAVEL TRAVELERS TRAVELERSINSURANCE TRUST TRV TT TUBE TUI TUNES TUSHU TV TVS TW TZ UA UBANK UBS UG UK UNICOM UNIVERSITY UNO UOL UPS US UY UZ VA VACATIONS VANA VANGUARD VC VE VEGAS VENTURES VERISIGN VERSICHERUNG VET VG VI VIAJES VIDEO VIG VIKING VILLAS VIN VIP VIRGIN VISA VISION VIVA VIVO VLAANDEREN VN VODKA VOLVO VOTE VOTING VOTO VOYAGE VU WALES WALMART WALTER WANG WANGGOU WATCH WATCHES WEATHER WEATHERCHANNEL WEBCAM WEBER WEBSITE WED WEDDING WEIBO WEIR WF WHOSWHO WIEN WIKI WILLIAMHILL WIN WINDOWS WINE WINNERS WME WOODSIDE WORK WORKS WORLD WOW WS WTC WTF XBOX XEROX XIHUAN XIN XN--11B4C3D XN--1CK2E1B XN--1QQW23A XN--2SCRJ9C XN--30RR7Y XN--3BST00M XN--3DS443G XN--3E0B707E XN--3HCRJ9C XN--3PXU8K XN--42C2D9A XN--45BR5CYL XN--45BRJ9C XN--45Q11C XN--4DBRK0CE XN--4GBRIM XN--54B7FTA0CC XN--55QW42G XN--55QX5D XN--5SU34J936BGSG XN--5TZM5G XN--6FRZ82G XN--6QQ986B3XL XN--80ADXHKS XN--80AO21A XN--80AQECDR1A XN--80ASEHDB XN--80ASWG XN--8Y0A063A XN--90A3AC XN--90AE XN--90AIS XN--9DBQ2A XN--9ET52U XN--9KRT00A XN--B4W605FERD XN--BCK1B9A5DRE4C XN--C1AVG XN--C2BR7G XN--CCK2B3B XN--CCKWCXETD XN--CG4BKI XN--CLCHC0EA0B2G2A9GCD XN--CZR694B XN--CZRS0T XN--CZRU2D XN--D1ACJ3B XN--D1ALF XN--E1A4C XN--ECKVDTC9D XN--EFVY88H XN--FCT429K XN--FHBEI XN--FIQ228C5HS XN--FIQ64B XN--FIQS8S XN--FIQZ9S XN--FJQ720A XN--FLW351E XN--FPCRJ9C3D XN--FZC2C9E2C XN--FZYS8D69UVGM XN--G2XX48C XN--GCKR3F0F XN--GECRJ9C XN--GK3AT1E XN--H2BREG3EVE XN--H2BRJ9C XN--H2BRJ9C8C XN--HXT814E XN--I1B6B1A6A2E XN--IMR513N XN--IO0A7I XN--J1AEF XN--J1AMH XN--J6W193G XN--JLQ480N2RG XN--JVR189M XN--KCRX77D1X4A XN--KPRW13D XN--KPRY57D XN--KPUT3I XN--L1ACC XN--LGBBAT1AD8J XN--MGB9AWBF XN--MGBA3A3EJT XN--MGBA3A4F16A XN--MGBA7C0BBN0A XN--MGBAAM7A8H XN--MGBAB2BD XN--MGBAH1A3HJKRD XN--MGBAI9AZGQP6J XN--MGBAYH7GPA XN--MGBBH1A XN--MGBBH1A71E XN--MGBC0A9AZCG XN--MGBCA7DZDO XN--MGBCPQ6GPA1A XN--MGBERP4A5D4AR XN--MGBGU82A XN--MGBI4ECEXP XN--MGBPL2FH XN--MGBT3DHD XN--MGBTX2B XN--MGBX4CD0AB XN--MIX891F XN--MK1BU44C XN--MXTQ1M XN--NGBC5AZD XN--NGBE9E0A XN--NGBRX XN--NODE XN--NQV7F XN--NQV7FS00EMA XN--NYQY26A XN--O3CW4H XN--OGBPF8FL XN--OTU796D XN--P1ACF XN--P1AI XN--PGBS0DH XN--PSSY2U XN--Q7CE6A XN--Q9JYB4C XN--QCKA1PMC XN--QXA6A XN--QXAM XN--RHQV96G XN--ROVU88B XN--RVC1E0AM3E XN--S9BRJ9C XN--SES554G XN--T60B56A XN--TCKWE XN--TIQ49XQYJ XN--UNUP4Y XN--VERMGENSBERATER-CTB XN--VERMGENSBERATUNG-PWB XN--VHQUV XN--VUQ861B XN--W4R85EL8FHU5DNRA XN--W4RS40L XN--WGBH1C XN--WGBL6A XN--XHQ521B XN--XKC2AL3HYE2A XN--XKC2DL3A5EE0H XN--Y9A3AQ XN--YFRO4I67O XN--YGBI2AMMX XN--ZFR164B XXX XYZ YACHTS YAHOO YAMAXUN YANDEX YE YODOBASHI YOGA YOKOHAMA YOU YOUTUBE YT YUN ZA ZAPPOS ZARA ZERO ZIP ZM ZONE ZUERICH ZW ================================================ FILE: hypothesis-python/src/hypothesis/version.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. __version_info__ = (6, 151, 9) __version__ = ".".join(map(str, __version_info__)) ================================================ FILE: hypothesis-python/tests/README.md ================================================ # Don't Panic The Hypothesis test suite is large, but we've written these notes to help you out. It's aimed at contributors (new and old!) who know they need to add tests *somewhere*, but aren't sure where - or maybe need some hints on what kinds of tests might be useful. Others might just be interested in how a testing library tests itself! ## The very short version - To improve code coverage (eg because `./build.sh check-coverage` or CI is failing), go to `cover/` - For longer / system / integration tests, look in `nocover/` - For tests that require an optional dependency, look in the directory named for that dependency. > [!NOTE] > If you get stuck, just ask a maintainer to help out by mentioning them on GitHub. We'd love to help - and also get feedback on how this document could be better! ## Some scenarios #### I'm adding or changing a strategy Check for a file specific to that strategy (eg `test_uuids.py` for the `uuids()` strategy). Write tests for all invalid argument handling in `test_direct_strategies.py`. Strategies with optional dependencies should go in `hypothesis.extras`, and the tests in their own module (ie not in `cover`). When you think you might be done, push and let our CI system point out any failing tests or non-covered code! #### I've made some internal changes That's not very specific - you should probably refer to the test-finding tips in the next section. Remember that `tests/cover` is reasonably quick unit-test style tests - you should consider writing more intensive integration tests too, but put them in `tests/nocover` with the others. ## Finding particular tests With the sheer size and variety in this directory finding a specific thing can be tricky. Tips: - Check for filenames that are relevant to your contribution. - Use `git grep` to search for keywords, e.g. the name of a strategy you've changed. - Deliberately break something related to your code, and see which tests fail. - Ask a maintainer! Sometimes the structure is just arbitrary, and other tactics don't work - but we *want* to help! ## About each group of tests Still here? Here's a note on what to expect in each directory. * `common/` * Useful shared testing code, including test setup and a few helper functions in `utils.py`. Also read up on [pytest](https://docs.pytest.org/en/latest/contents.html>) features such as `mark.parametrize`, `mark.skipif`, and `raises` for other functions that are often useful when writing tests. * `conjecture/` * As for `cover/`, but specific to `hypothesis.internal.conjecture`. * `cover/` * The home of enough tests to get 100% branch coverage, as quickly as possible without compromising on test power. This can be an intimidating target, but it's entirely achievable and the maintainers are (still) here to help. * This directory alone has around two-thirds of the tests for Hypothesis (~8k of ~12k lines of code). If you're adding or fixing tests, chances are therefore good that they're in here! * `datetime/` * Tests which depend on the `pytz` or `dateutil` packages for timezones. * `django/` * Tests for the Django extra. Includes a toy application, to give us lots of models to generate. * `lark/` * Tests for the Lark extra for context-free grammars, which depend on the `lark` package. * `nocover/` * More expensive and longer-running tests, typically used to test trickier interactions or check for regressions in expensive bugs. Lots of tests about how values shrink, databases, compatibility, etc. * New tests that are not required for full coverage of code branches or behaviour should also go in `nocover`, to keep `cover` reasonably fast. * `numpy/` * Tests for the Numpy extra. * `pandas/` * Tests for the Pandas extra. * `pytest/` * Hypothesis has excellent integration with `pytest`, though we are careful to support other test runners such as unittest. This is where we test that our pytest integration is working properly. * `quality/` * Tests that various hard-to-find examples do in fact get found by Hypothesis, as well as some stuff about example shrinking. Mostly intended for tests of the form "Hypothesis finds an example of this condition" + assertions about which example it finds. ================================================ FILE: hypothesis-python/tests/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/array_api/README.md ================================================ This folder contains tests for `hypothesis.extra.array_api`. ## Mocked array module A mock of the Array API namespace exists as `mock_xp` in `extra.array_api`. This wraps NumPy-proper to conform it to the *draft* spec, where `array_api_strict` might not. This is not a fully compliant wrapper, but conforms enough for the purposes of testing. ## Running against different array modules You can test other array modules which adopt the Array API via the `HYPOTHESIS_TEST_ARRAY_API` environment variable. There are two recognized options: * `"default"`: uses the mock. * `"all"`: uses all array modules found via entry points, _and_ the mock. If neither of these, the test suite will then try resolve the variable like so: 1. If the variable matches a name of an available entry point, load said entry point. 2. If the variables matches a valid import path, import said path. For example, to specify NumPy's Array API implementation[^1], you could use its entry point (**1.**), HYPOTHESIS_TEST_ARRAY_API=numpy pytest tests/array_api or use the import path (**2.**), HYPOTHESIS_TEST_ARRAY_API=numpy.array_api pytest tests/array_api The former method is more ergonomic, but as entry points are optional for adopting the Array API, you will need to use the latter method for libraries that opt-out. ## Running against different API versions You can specify the `api_version` to use when testing array modules via the `HYPOTHESIS_TEST_ARRAY_API_VERSION` environment variable. There is one recognized option: * `"default"`: infers the latest API version for each array module. Otherwise the test suite will use the variable as the `api_version` argument for `make_strategies_namespace()`. In the future we intend to support running tests against multiple API versioned namespaces, likely with an additional recognized option that infers all supported versions. [^1]: Note NumPy will likely remove `numpy.array_api` in the future ([NEP 56](https://github.com/numpy/numpy/pull/25542)) in favour of the third-party [`array-api-strict`](https://github.com/data-apis/array-api-strict) library. ================================================ FILE: hypothesis-python/tests/array_api/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/array_api/common.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from importlib.metadata import EntryPoint, entry_points # type: ignore from typing import Literal import pytest from hypothesis.extra.array_api import ( COMPLEX_NAMES, REAL_NAMES, RELEASED_VERSIONS, NominalVersion, ) from hypothesis.internal.floats import next_up __all__ = [ "MIN_VER_FOR_COMPLEX", "dtype_name_params", "flushes_to_zero", "installed_array_modules", ] MIN_VER_FOR_COMPLEX: NominalVersion = "2022.12" if len(RELEASED_VERSIONS) > 1: assert MIN_VER_FOR_COMPLEX == RELEASED_VERSIONS[1] def installed_array_modules() -> dict[str, EntryPoint]: """Returns a dictionary of array module names paired to their entry points A convenience wrapper for importlib.metadata.entry_points(). It has the added benefit of working with both the original dict interface and the new select interface, so this can be used warning-free in all modern Python versions. """ return {ep.name: ep for ep in entry_points(group="array_api")} def flushes_to_zero(xp, width: Literal[32, 64]) -> bool: """Infer whether build of array module has its float dtype of the specified width flush subnormals to zero We do this per-width because compilers might FTZ for one dtype but allow subnormals in the other. """ if width not in [32, 64]: raise ValueError(f"{width=}, but should be either 32 or 64") dtype = getattr(xp, f"float{width}") return bool(xp.asarray(next_up(0.0, width=width), dtype=dtype) == 0) dtype_name_params = ["bool", *REAL_NAMES] for name in COMPLEX_NAMES: param = pytest.param(name, marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX)) dtype_name_params.append(param) ================================================ FILE: hypothesis-python/tests/array_api/conftest.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import warnings from importlib import import_module from os import getenv from types import ModuleType, SimpleNamespace import pytest from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.extra.array_api import ( NOMINAL_VERSIONS, NominalVersion, make_strategies_namespace, mock_xp, ) from tests.array_api.common import installed_array_modules # See README.md in regards to the env variables test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default") test_version_option = getenv("HYPOTHESIS_TEST_ARRAY_API_VERSION", "default") if test_version_option != "default" and test_version_option not in NOMINAL_VERSIONS: raise ValueError( f"HYPOTHESIS_TEST_ARRAY_API_VERSION='{test_version_option}' is not " f"'default' or a valid api_version {NOMINAL_VERSIONS}." ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=HypothesisWarning) mock_version = "draft" if test_version_option == "default" else test_version_option mock_xps = make_strategies_namespace(mock_xp, api_version=mock_version) api_version = None if test_version_option == "default" else test_version_option class InvalidArgumentWarning(UserWarning): """Custom warning so we can bypass our global capturing""" name_to_entry_point = installed_array_modules() xp_and_xps_pairs: list[tuple[ModuleType, SimpleNamespace]] = [] with warnings.catch_warnings(): # We ignore all warnings here as many array modules warn on import. Ideally # we would just ignore ImportWarning, but no one seems to use it! warnings.simplefilter("ignore") warnings.simplefilter("default", category=InvalidArgumentWarning) # We go through the steps described in README.md to define `xp_xps_pairs`, # which contains the array module(s) to be run against the test suite, along # with their respective strategy namespaces. if test_xp_option == "default": xp_and_xps_pairs = [(mock_xp, mock_xps)] elif test_xp_option == "all": if len(name_to_entry_point) == 0: raise ValueError( "HYPOTHESIS_TEST_ARRAY_API='all', but no entry points where found" ) xp_and_xps_pairs = [(mock_xp, mock_xps)] for ep in name_to_entry_point.values(): xp = ep.load() try: xps = make_strategies_namespace(xp, api_version=api_version) except InvalidArgument as e: warnings.warn(str(e), InvalidArgumentWarning, stacklevel=1) else: xp_and_xps_pairs.append((xp, xps)) elif test_xp_option in name_to_entry_point: ep = name_to_entry_point[test_xp_option] xp = ep.load() xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs = [(xp, xps)] else: try: xp = import_module(test_xp_option) except ImportError as e: raise ValueError( f"HYPOTHESIS_TEST_ARRAY_API='{test_xp_option}' is not a valid " "option ('default' or 'all'), name of an available entry point, " "or a valid import path." ) from e else: xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs = [(xp, xps)] def pytest_generate_tests(metafunc): xp_params = [] xp_and_xps_params = [] for xp, xps in xp_and_xps_pairs: xp_params.append(pytest.param(xp, id=xp.__name__)) xp_and_xps_params.append( pytest.param(xp, xps, id=f"{xp.__name__}-{xps.api_version}") ) if "xp" in metafunc.fixturenames: if "xps" in metafunc.fixturenames: metafunc.parametrize("xp, xps", xp_and_xps_params) else: metafunc.parametrize("xp", xp_params) def pytest_collection_modifyitems(config, items): for item in items: if "xps" in item.fixturenames: markers = [m for m in item.own_markers if m.name == "xp_min_version"] if markers: assert len(markers) == 1 # sanity check min_version: NominalVersion = markers[0].args[0] xps_version: NominalVersion = item.callspec.params["xps"].api_version if xps_version < min_version: item.add_marker( pytest.mark.skip(reason=f"requires api_version=>{min_version}") ) ================================================ FILE: hypothesis-python/tests/array_api/test_argument_validation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis.errors import InvalidArgument from hypothesis.extra.array_api import NominalVersion, make_strategies_namespace from tests.array_api.common import MIN_VER_FOR_COMPLEX from tests.common.debug import check_can_generate_examples def e(name, *, _min_version: NominalVersion | None = None, **kwargs): kw = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) id_ = f"{name}({kw})" if _min_version is None: marks = () else: marks = pytest.mark.xp_min_version(_min_version) return pytest.param(name, kwargs, id=id_, marks=marks) @pytest.mark.parametrize( ("strat_name", "kwargs"), [ e("arrays", dtype=1, shape=5), e("arrays", dtype=None, shape=5), e("arrays", dtype="int8", shape=(0.5,)), e("arrays", dtype="int8", shape=1, fill=3), e("arrays", dtype="int8", shape=1, elements="not a strategy"), e("arrays", dtype="int8", shape="not a shape or strategy"), e("array_shapes", min_side=2, max_side=1), e("array_shapes", min_dims=3, max_dims=2), e("array_shapes", min_dims=-1), e("array_shapes", min_side=-1), e("array_shapes", min_side="not an int"), e("array_shapes", max_side="not an int"), e("array_shapes", min_dims="not an int"), e("array_shapes", max_dims="not an int"), e("from_dtype", dtype=1), e("from_dtype", dtype=None), e("from_dtype", dtype="int8", min_value="not an int"), e("from_dtype", dtype="int8", max_value="not an int"), e("from_dtype", dtype="float32", min_value="not a float"), e("from_dtype", dtype="float32", max_value="not a float"), e("from_dtype", dtype="int8", min_value=10, max_value=5), e("from_dtype", dtype="float32", min_value=10, max_value=5), e("from_dtype", dtype="int8", min_value=-999), e("from_dtype", dtype="int8", max_value=-999), e("from_dtype", dtype="int8", min_value=999), e("from_dtype", dtype="int8", max_value=999), e("from_dtype", dtype="uint8", min_value=-999), e("from_dtype", dtype="uint8", max_value=-999), e("from_dtype", dtype="uint8", min_value=999), e("from_dtype", dtype="uint8", max_value=999), e("from_dtype", dtype="float32", min_value=-4e38), e("from_dtype", dtype="float32", max_value=-4e38), e("from_dtype", dtype="float32", min_value=4e38), e("from_dtype", dtype="float32", max_value=4e38), e("integer_dtypes", sizes=()), e("integer_dtypes", sizes=(3,)), e("unsigned_integer_dtypes", sizes=()), e("unsigned_integer_dtypes", sizes=(3,)), e("floating_dtypes", sizes=()), e("floating_dtypes", sizes=(3,)), e("complex_dtypes", _min_version=MIN_VER_FOR_COMPLEX, sizes=()), e("complex_dtypes", _min_version=MIN_VER_FOR_COMPLEX, sizes=(3,)), e("valid_tuple_axes", ndim=-1), e("valid_tuple_axes", ndim=2, min_size=-1), e("valid_tuple_axes", ndim=2, min_size=3, max_size=10), e("valid_tuple_axes", ndim=2, min_size=2, max_size=1), e("valid_tuple_axes", ndim=2.0, min_size=2, max_size=1), e("valid_tuple_axes", ndim=2, min_size=1.0, max_size=2), e("valid_tuple_axes", ndim=2, min_size=1, max_size=2.0), e("valid_tuple_axes", ndim=2, min_size=1, max_size=3), e("broadcastable_shapes", shape="a"), e("broadcastable_shapes", shape=(2, 2), min_side="a"), e("broadcastable_shapes", shape=(2, 2), min_dims="a"), e("broadcastable_shapes", shape=(2, 2), max_side="a"), e("broadcastable_shapes", shape=(2, 2), max_dims="a"), e("broadcastable_shapes", shape=(2, 2), min_side=-1), e("broadcastable_shapes", shape=(2, 2), min_dims=-1), e("broadcastable_shapes", shape=(2, 2), min_side=1, max_side=0), e("broadcastable_shapes", shape=(2, 2), min_dims=1, max_dims=0), e( "broadcastable_shapes", # max_side too small shape=(5, 1), min_dims=2, max_dims=4, min_side=2, max_side=3, ), e( "broadcastable_shapes", # min_side too large shape=(0, 1), min_dims=2, max_dims=4, min_side=2, max_side=3, ), e( "broadcastable_shapes", # default max_dims unsatisfiable shape=(5, 3, 2, 1), min_dims=3, max_dims=None, min_side=2, max_side=3, ), e( "broadcastable_shapes", # default max_dims unsatisfiable shape=(0, 3, 2, 1), min_dims=3, max_dims=None, min_side=2, max_side=3, ), e("mutually_broadcastable_shapes", num_shapes=0), e("mutually_broadcastable_shapes", num_shapes="a"), e("mutually_broadcastable_shapes", num_shapes=2, base_shape="a"), e( "mutually_broadcastable_shapes", # min_side is invalid type num_shapes=2, min_side="a", ), e( "mutually_broadcastable_shapes", # min_dims is invalid type num_shapes=2, min_dims="a", ), e( "mutually_broadcastable_shapes", # max_side is invalid type num_shapes=2, max_side="a", ), e( "mutually_broadcastable_shapes", # max_side is invalid type num_shapes=2, max_dims="a", ), e( "mutually_broadcastable_shapes", # min_side is out of domain num_shapes=2, min_side=-1, ), e( "mutually_broadcastable_shapes", # min_dims is out of domain num_shapes=2, min_dims=-1, ), e( "mutually_broadcastable_shapes", # max_side < min_side num_shapes=2, min_side=1, max_side=0, ), e( "mutually_broadcastable_shapes", # max_dims < min_dims num_shapes=2, min_dims=1, max_dims=0, ), e( "mutually_broadcastable_shapes", # max_side too small num_shapes=2, base_shape=(5, 1), min_dims=2, max_dims=4, min_side=2, max_side=3, ), e( "mutually_broadcastable_shapes", # min_side too large num_shapes=2, base_shape=(0, 1), min_dims=2, max_dims=4, min_side=2, max_side=3, ), e( "mutually_broadcastable_shapes", # user-specified max_dims unsatisfiable num_shapes=1, base_shape=(5, 3, 2, 1), min_dims=3, max_dims=4, min_side=2, max_side=3, ), e( "mutually_broadcastable_shapes", # user-specified max_dims unsatisfiable num_shapes=2, base_shape=(0, 3, 2, 1), min_dims=3, max_dims=4, min_side=2, max_side=3, ), e("indices", shape=0), e("indices", shape=("1", "2")), e("indices", shape=(0, -1)), e("indices", shape=(0, 0), allow_newaxis=None), e("indices", shape=(0, 0), allow_ellipsis=None), e("indices", shape=(0, 0), min_dims=-1), e("indices", shape=(0, 0), min_dims=1.0), e("indices", shape=(0, 0), max_dims=-1), e("indices", shape=(0, 0), max_dims=1.0), e("indices", shape=(0, 0), min_dims=2, max_dims=1), e("indices", shape=(3, 3, 3), min_dims=4), e("indices", shape=(3, 3, 3), max_dims=5), e("indices", shape=5, min_dims=0), e("indices", shape=(5,), min_dims=2), e("indices", shape=(5,), max_dims=2), ], ) def test_raise_invalid_argument(xp, xps, strat_name, kwargs): """Strategies raise helpful error with invalid arguments.""" strat_func = getattr(xps, strat_name) strat = strat_func(**kwargs) with pytest.raises(InvalidArgument): check_can_generate_examples(strat) @pytest.mark.parametrize("api_version", [..., "latest", "1970.01", 42]) def test_make_strategies_namespace_raise_invalid_argument(xp, api_version): """Function raises helpful error with invalid arguments.""" with pytest.raises(InvalidArgument): make_strategies_namespace(xp, api_version=api_version) ================================================ FILE: hypothesis-python/tests/array_api/test_arrays.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import pytest from hypothesis import given, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES from hypothesis.internal.floats import width_smallest_normals from tests.array_api.common import ( MIN_VER_FOR_COMPLEX, dtype_name_params, flushes_to_zero, ) from tests.common.debug import ( assert_all_examples, check_can_generate_examples, find_any, minimal, ) from tests.common.utils import flaky def skip_on_missing_unique_values(xp): if not hasattr(xp, "unique_values"): pytest.mark.skip("xp.unique_values() is not required to exist") def xfail_on_indistinct_nans(xp): """ xp.unique_value() should return distinct NaNs - if not, tests that (rightly) assume such behaviour will likely fail. For example, NumPy 1.22 treats NaNs as indistinct, so tests that use this function will be marked as xfail. See https://mail.python.org/pipermail/numpy-discussion/2021-August/081995.html """ skip_on_missing_unique_values(xp) two_nans = xp.asarray([float("nan"), float("nan")]) if xp.unique_values(two_nans).size != 2: pytest.xfail("NaNs not distinct") @pytest.mark.parametrize("dtype_name", dtype_name_params) def test_draw_arrays_from_dtype(xp, xps, dtype_name): """Draw arrays from dtypes.""" dtype = getattr(xp, dtype_name) assert_all_examples(xps.arrays(dtype, ()), lambda x: x.dtype == dtype) @pytest.mark.parametrize("dtype_name", dtype_name_params) def test_draw_arrays_from_scalar_names(xp, xps, dtype_name): """Draw arrays from dtype names.""" dtype = getattr(xp, dtype_name) assert_all_examples(xps.arrays(dtype_name, ()), lambda x: x.dtype == dtype) @given(data=st.data()) def test_draw_arrays_from_shapes(xp, xps, data): """Draw arrays from shapes.""" shape = data.draw(xps.array_shapes()) x = data.draw(xps.arrays(xp.int8, shape)) assert x.ndim == len(shape) assert x.shape == shape @given(data=st.data()) def test_draw_arrays_from_int_shapes(xp, xps, data): """Draw arrays from integers as shapes.""" size = data.draw(st.integers(0, 10)) x = data.draw(xps.arrays(xp.int8, size)) assert x.shape == (size,) @pytest.mark.parametrize( "strat_name", [ "scalar_dtypes", "boolean_dtypes", "integer_dtypes", "unsigned_integer_dtypes", "floating_dtypes", "real_dtypes", pytest.param( "complex_dtypes", marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) ), ], ) def test_draw_arrays_from_dtype_strategies(xp, xps, strat_name): """Draw arrays from dtype strategies.""" strat_func = getattr(xps, strat_name) strat = strat_func() find_any(xps.arrays(strat, ())) @settings(deadline=None) @given(data=st.data()) def test_draw_arrays_from_dtype_name_strategies(xp, xps, data): """Draw arrays from dtype name strategies.""" all_names = ("bool", *REAL_NAMES) if xps.api_version > "2021.12": all_names += COMPLEX_NAMES sample_names = data.draw( st.lists(st.sampled_from(all_names), min_size=1, unique=True) ) find_any(xps.arrays(st.sampled_from(sample_names), ())) def test_generate_arrays_from_shapes_strategy(xp, xps): """Generate arrays from shapes strategy.""" find_any(xps.arrays(xp.int8, xps.array_shapes())) def test_generate_arrays_from_integers_strategy_as_shape(xp, xps): """Generate arrays from integers strategy as shapes strategy.""" find_any(xps.arrays(xp.int8, st.integers(0, 100))) def test_generate_arrays_from_zero_dimensions(xp, xps): """Generate arrays from empty shape.""" assert_all_examples(xps.arrays(xp.int8, ()), lambda x: x.shape == ()) @given(data=st.data()) def test_generate_arrays_from_zero_sided_shapes(xp, xps, data): """Generate arrays from shapes with at least one 0-sized dimension.""" shape = data.draw(xps.array_shapes(min_side=0).filter(lambda s: 0 in s)) arr = data.draw(xps.arrays(xp.int8, shape)) assert arr.shape == shape def test_generate_arrays_from_unsigned_ints(xp, xps): """Generate arrays from unsigned integer dtype.""" assert_all_examples(xps.arrays(xp.uint32, (5, 5)), lambda x: xp.all(x >= 0)) # Ensure we're not just picking non-negative signed integers signed_max = xp.iinfo(xp.int32).max find_any(xps.arrays(xp.uint32, (5, 5)), lambda x: xp.any(x > signed_max)) def test_generate_arrays_from_0d_arrays(xp, xps): """Generate arrays from 0d array elements.""" assert_all_examples( xps.arrays( dtype=xp.uint8, shape=(5, 5), elements=xps.from_dtype(xp.uint8).map( lambda e: xp.asarray(e, dtype=xp.uint8) ), ), lambda x: x.shape == (5, 5), ) def test_minimize_arrays_with_default_dtype_shape_strategies(xp, xps): """Strategy with default scalar_dtypes and array_shapes strategies minimize to a boolean 1-dimensional array of size 1.""" smallest = minimal(xps.arrays(xps.scalar_dtypes(), xps.array_shapes())) assert smallest.shape == (1,) assert smallest.dtype == xp.bool assert not xp.any(smallest) def test_minimize_arrays_with_0d_shape_strategy(xp, xps): """Strategy with shape strategy that can generate empty tuples minimizes to 0d arrays.""" smallest = minimal(xps.arrays(xp.int8, xps.array_shapes(min_dims=0))) assert smallest.shape == () @pytest.mark.parametrize("dtype", dtype_name_params[1:]) def test_minimizes_numeric_arrays(xp, xps, dtype): """Strategies with numeric dtypes minimize to zero-filled arrays.""" smallest = minimal(xps.arrays(dtype, (2, 2))) assert xp.all(smallest == 0) def test_minimize_large_uint_arrays(xp, xps): """Strategy with uint dtype and largely sized shape minimizes to a good example.""" if not hasattr(xp, "nonzero"): pytest.skip("optional API") smallest = minimal(xps.arrays(xp.uint8, 100), lambda x: xp.any(x) and not xp.all(x)) assert xp.all(xp.logical_or(smallest == 0, smallest == 1)) idx = xp.nonzero(smallest)[0] assert idx.size in (1, smallest.size - 1) @pytest.mark.filterwarnings("ignore::RuntimeWarning") @flaky(max_runs=50, min_passes=1) def test_minimize_float_arrays(xp, xps): """Strategy with float dtype minimizes to a good example. We filter runtime warnings and expect flaky array generation for specifically NumPy - this behaviour may not be required when testing with other array libraries. """ smallest = minimal(xps.arrays(xp.float32, 50), lambda x: xp.sum(x) >= 1.0) # TODO_IR the shrinker gets stuck when the first failure is math.inf, because # downcasting inf to a float32 overflows, triggering rejection sampling which # is then immediately not a shrink (specifically it overruns the attempt data). # # this should be resolved by adding float widths to the ir. assert xp.sum(smallest) in (1, 50) or all(math.isinf(v) for v in smallest) def test_minimizes_to_fill(xp, xps): """Strategy with single fill value minimizes to arrays only containing said fill value.""" smallest = minimal(xps.arrays(xp.float32, 10, fill=st.just(3.0))) assert xp.all(smallest == 3.0) def test_generate_unique_arrays(xp, xps): """Generates unique arrays.""" skip_on_missing_unique_values(xp) assert_all_examples( xps.arrays(xp.int8, st.integers(0, 20), unique=True), lambda x: xp.unique_values(x).size == x.size, ) def test_cannot_draw_unique_arrays_with_too_small_elements(xp, xps): """Unique strategy with elements strategy range smaller than its size raises helpful error.""" with pytest.raises(InvalidArgument): check_can_generate_examples( xps.arrays(xp.int8, 10, elements=st.integers(0, 5), unique=True) ) def test_cannot_fill_arrays_with_non_castable_value(xp, xps): """Strategy with fill not castable to dtype raises helpful error.""" with pytest.raises(InvalidArgument): check_can_generate_examples( xps.arrays(xp.int8, 10, fill=st.just("not a castable value")) ) def test_generate_unique_arrays_with_high_collision_elements(xp, xps): """Generates unique arrays with just elements of 0.0 and NaN fill.""" @given( xps.arrays( dtype=xp.float32, shape=st.integers(0, 20), elements=st.just(0.0), fill=st.just(xp.nan), unique=True, ) ) def test(x): zero_mask = x == 0.0 assert xp.sum(xp.astype(zero_mask, xp.uint8)) <= 1 test() def test_generate_unique_arrays_using_all_elements(xp, xps): """Unique strategy with elements strategy range equal to its size will only generate arrays with one of each possible element.""" skip_on_missing_unique_values(xp) assert_all_examples( xps.arrays(xp.int8, (4,), elements=st.integers(0, 3), unique=True), lambda x: xp.unique_values(x).size == x.size, ) def test_may_fill_unique_arrays_with_nan(xp, xps): """Unique strategy with NaN fill can generate arrays holding NaNs.""" find_any( xps.arrays( dtype=xp.float32, shape=10, elements={"allow_nan": False}, unique=True, fill=st.just(xp.nan), ), lambda x: xp.any(xp.isnan(x)), ) def test_may_not_fill_unique_array_with_non_nan(xp, xps): """Unique strategy with just fill elements of 0.0 raises helpful error.""" strat = xps.arrays( dtype=xp.float32, shape=10, elements={"allow_nan": False}, unique=True, fill=st.just(0.0), ) with pytest.raises(InvalidArgument): check_can_generate_examples(strat) def test_floating_point_array(): import warnings from hypothesis.extra.array_api import make_strategies_namespace try: with warnings.catch_warnings(): warnings.simplefilter("ignore") import numpy.array_api as nxp except ModuleNotFoundError: import numpy as nxp xps = make_strategies_namespace(nxp) dtypes = xps.floating_dtypes() | xps.complex_dtypes() strat = xps.arrays(dtype=dtypes, shape=10) check_can_generate_examples(strat) @pytest.mark.parametrize( "kwargs", [ {"elements": st.just(300)}, {"elements": st.nothing(), "fill": st.just(300)}, ], ) def test_may_not_use_overflowing_integers(xp, xps, kwargs): """Strategy with elements strategy range outside the dtype's bounds raises helpful error.""" with pytest.raises(InvalidArgument): check_can_generate_examples(xps.arrays(dtype=xp.int8, shape=1, **kwargs)) @pytest.mark.parametrize("fill", [False, True]) @pytest.mark.parametrize( "dtype, strat", [ ("float32", st.floats(min_value=10**40, allow_infinity=False)), ("float64", st.floats(min_value=10**40, allow_infinity=False)), pytest.param( "complex64", st.complex_numbers(min_magnitude=10**300, allow_infinity=False), marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX), ), ], ) def test_may_not_use_unrepresentable_elements(xp, xps, fill, dtype, strat): """Strategy with elements not representable by the dtype raises helpful error.""" if fill: kw = {"elements": st.nothing(), "fill": strat} else: kw = {"elements": strat} with pytest.raises(InvalidArgument): check_can_generate_examples(xps.arrays(dtype=dtype, shape=1, **kw)) def test_floats_can_be_constrained(xp, xps): """Strategy with float dtype and specified elements strategy range (inclusive) generates arrays with elements inside said range.""" assert_all_examples( xps.arrays( dtype=xp.float32, shape=10, elements={"min_value": 0, "max_value": 1} ), lambda x: xp.all(x >= 0) and xp.all(x <= 1), ) def test_floats_can_be_constrained_excluding_endpoints(xp, xps): """Strategy with float dtype and specified elements strategy range (exclusive) generates arrays with elements inside said range.""" assert_all_examples( xps.arrays( dtype=xp.float32, shape=10, elements={ "min_value": 0, "max_value": 1, "exclude_min": True, "exclude_max": True, }, ), lambda x: xp.all(x > 0) and xp.all(x < 1), ) def test_is_still_unique_with_nan_fill(xp, xps): """Unique strategy with NaN fill generates unique arrays.""" skip_on_missing_unique_values(xp) xfail_on_indistinct_nans(xp) assert_all_examples( xps.arrays( dtype=xp.float32, elements={"allow_nan": False}, shape=10, unique=True, fill=st.just(xp.nan), ), lambda x: xp.unique_values(x).size == x.size, ) def test_unique_array_with_fill_can_use_all_elements(xp, xps): """Unique strategy with elements range equivalent to its size and NaN fill can generate arrays with all possible values.""" skip_on_missing_unique_values(xp) xfail_on_indistinct_nans(xp) find_any( xps.arrays( dtype=xp.float32, shape=10, unique=True, elements=st.integers(1, 9), fill=st.just(xp.nan), ), lambda x: xp.unique_values(x).size == x.size, ) def test_generate_unique_arrays_without_fill(xp, xps): """Generate arrays from unique strategy with no fill. Covers the collision-related branches for fully dense unique arrays. Choosing 25 of 256 possible values means we're almost certain to see collisions thanks to the birthday paradox, but finding unique values should still be easy. """ skip_on_missing_unique_values(xp) assert_all_examples( xps.arrays(dtype=xp.uint8, shape=25, unique=True, fill=st.nothing()), lambda x: xp.unique_values(x).size == x.size, ) def test_efficiently_generate_unique_arrays_using_all_elements(xp, xps): """Unique strategy with elements strategy range equivalent to its size generates arrays with all possible values. Generation is not too slow. Avoids the birthday paradox with UniqueSampledListStrategy. """ skip_on_missing_unique_values(xp) assert_all_examples( xps.arrays(dtype=xp.int8, shape=255, unique=True), lambda x: xp.unique_values(x).size == x.size, ) @given(st.data(), st.integers(-100, 100), st.integers(1, 100)) def test_array_element_rewriting(xp, xps, data, start, size): """Unique strategy generates arrays with expected elements.""" x = data.draw( xps.arrays( dtype=xp.int64, shape=size, elements=st.integers(start, start + size - 1), unique=True, ) ) x_set_expect = xp.arange(start, start + size, dtype=xp.int64) x_set = xp.sort(xp.unique_values(x)) assert xp.all(x_set == x_set_expect) def test_generate_0d_arrays_with_no_fill(xp, xps): """Generate arrays with zero-dimensions and no fill.""" assert_all_examples( xps.arrays(xp.bool, (), fill=st.nothing()), lambda x: x.dtype == xp.bool and x.shape == (), ) @pytest.mark.parametrize("dtype", ["float32", "float64"]) @pytest.mark.parametrize("low", [-2.0, -1.0, 0.0, 1.0]) @given(st.data()) def test_excluded_min_in_float_arrays(xp, xps, dtype, low, data): """Strategy with elements strategy excluding min does not generate arrays with elements less or equal to said min.""" strat = xps.arrays( dtype=dtype, shape=(), elements={ "min_value": low, "max_value": low + 1, "exclude_min": True, }, ) x = data.draw(strat, label="array") assert xp.all(x > low) @st.composite def distinct_int64_integers(draw): used = draw(st.shared(st.builds(set), key="distinct_int64_integers.used")) i = draw(st.integers(-(2**63), 2**63 - 1).filter(lambda x: x not in used)) used.add(i) return i def test_does_not_reuse_distinct_integers(xp, xps): """Strategy with distinct integer elements strategy generates arrays with distinct values.""" skip_on_missing_unique_values(xp) assert_all_examples( xps.arrays(xp.int64, 10, elements=distinct_int64_integers()), lambda x: xp.unique_values(x).size == x.size, ) def test_may_reuse_distinct_integers_if_asked(xp, xps): """Strategy with shared elements and fill strategies of distinct integers may generate arrays with non-distinct values.""" skip_on_missing_unique_values(xp) find_any( xps.arrays( xp.int64, 10, elements=distinct_int64_integers(), fill=distinct_int64_integers(), ), lambda x: xp.unique_values(x).size < x.size, ) def test_subnormal_elements_validation(xp, xps): """Strategy with subnormal elements strategy is correctly validated. For FTZ builds of array modules, a helpful error should raise. Conversely, for builds of array modules which support subnormals, the strategy should generate arrays without raising. """ elements = { "min_value": 0.0, "max_value": width_smallest_normals[32], "exclude_min": True, "exclude_max": True, "allow_subnormal": True, } strat = xps.arrays(xp.float32, 10, elements=elements) if flushes_to_zero(xp, width=32): with pytest.raises(InvalidArgument, match="Generated subnormal float"): check_can_generate_examples(strat) else: check_can_generate_examples(strat) ================================================ FILE: hypothesis-python/tests/array_api/test_from_dtype.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import pytest from hypothesis.extra.array_api import find_castable_builtin_for_dtype from hypothesis.internal.floats import width_smallest_normals from tests.array_api.common import dtype_name_params, flushes_to_zero from tests.common.debug import ( assert_all_examples, assert_no_examples, find_any, minimal, ) @pytest.mark.parametrize("dtype_name", dtype_name_params) def test_strategies_have_reusable_values(xp, xps, dtype_name): """Inferred strategies have reusable values.""" strat = xps.from_dtype(dtype_name) assert strat.has_reusable_values @pytest.mark.parametrize("dtype_name", dtype_name_params) def test_produces_castable_instances_from_dtype(xp, xps, dtype_name): """Strategies inferred by dtype generate values of a builtin type castable to the dtype.""" dtype = getattr(xp, dtype_name) builtin = find_castable_builtin_for_dtype(xp, xps.api_version, dtype) assert_all_examples(xps.from_dtype(dtype), lambda v: isinstance(v, builtin)) @pytest.mark.parametrize("dtype_name", dtype_name_params) def test_produces_castable_instances_from_name(xp, xps, dtype_name): """Strategies inferred by dtype name generate values of a builtin type castable to the dtype.""" dtype = getattr(xp, dtype_name) builtin = find_castable_builtin_for_dtype(xp, xps.api_version, dtype) assert_all_examples(xps.from_dtype(dtype_name), lambda v: isinstance(v, builtin)) @pytest.mark.parametrize("dtype_name", dtype_name_params) def test_passing_inferred_strategies_in_arrays(xp, xps, dtype_name): """Inferred strategies usable in arrays strategy.""" elements = xps.from_dtype(dtype_name) find_any(xps.arrays(dtype_name, 10, elements=elements)) @pytest.mark.parametrize( "dtype, kwargs, predicate", [ # Floating point: bounds, exclusive bounds, and excluding nonfinites ("float32", {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2), ( "float32", {"min_value": 1, "max_value": 2, "exclude_min": True, "exclude_max": True}, lambda x: 1 < x < 2, ), ("float32", {"allow_nan": False}, lambda x: not math.isnan(x)), ("float32", {"allow_infinity": False}, lambda x: not math.isinf(x)), ("float32", {"allow_nan": False, "allow_infinity": False}, math.isfinite), # Integer bounds, limited to the representable range ("int8", {"min_value": -1, "max_value": 1}, lambda x: -1 <= x <= 1), ("uint8", {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2), ], ) def test_from_dtype_with_kwargs(xp, xps, dtype, kwargs, predicate): """Strategies inferred with kwargs generate values in bounds.""" strat = xps.from_dtype(dtype, **kwargs) assert_all_examples(strat, predicate) def test_can_minimize_floats(xp, xps): """Inferred float strategy minimizes to a good example.""" smallest = minimal(xps.from_dtype(xp.float32), lambda n: n >= 1.0) # TODO_IR should be resolved by float widths on the ir, see other TODO_IR comments assert smallest in {1, math.inf} smallest_normal = width_smallest_normals[32] @pytest.mark.parametrize( "kwargs", [ {}, {"min_value": -1}, {"max_value": 1}, {"min_value": -1, "max_value": 1}, ], ) def test_subnormal_generation(xp, xps, kwargs): """Generation of subnormals is dependent on FTZ behaviour of array module.""" strat = xps.from_dtype(xp.float32, **kwargs).filter(lambda n: n != 0) if flushes_to_zero(xp, width=32): assert_no_examples(strat, lambda n: -smallest_normal < n < smallest_normal) else: find_any(strat, lambda n: -smallest_normal < n < smallest_normal) ================================================ FILE: hypothesis-python/tests/array_api/test_indices.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import pytest from hypothesis import assume, given, note, strategies as st from hypothesis.extra._array_helpers import NDIM_MAX from tests.common.debug import assert_all_examples, find_any @pytest.mark.parametrize( "condition", [ lambda ix: Ellipsis in ix, lambda ix: Ellipsis not in ix, lambda ix: None in ix, lambda ix: None not in ix, ], ) def test_generate_optional_indices(xp, xps, condition): """Strategy can generate indices with optional values.""" strat = ( xps.array_shapes(min_dims=1, max_dims=32) .flatmap(lambda s: xps.indices(s, allow_newaxis=True)) .map(lambda idx: idx if isinstance(idx, tuple) else (idx,)) ) find_any(strat, condition) def test_cannot_generate_newaxis_when_disabled(xp, xps): """Strategy does not generate newaxis when disabled (i.e. the default).""" assert_all_examples( xps.indices((3, 3, 3)), lambda idx: idx == ... or None not in idx ) def test_generate_indices_for_0d_shape(xp, xps): """Strategy only generates empty tuples or Ellipsis as indices for an empty shape.""" assert_all_examples( xps.indices(shape=(), allow_ellipsis=True), lambda idx: idx in [(), Ellipsis, (Ellipsis,)], ) def test_generate_tuples_and_non_tuples_for_1d_shape(xp, xps): """Strategy can generate tuple and non-tuple indices with a 1-dimensional shape.""" strat = xps.indices(shape=(1,), allow_ellipsis=True) find_any(strat, lambda ix: isinstance(ix, tuple)) find_any(strat, lambda ix: not isinstance(ix, tuple)) def test_generate_long_ellipsis(xp, xps): """Strategy can replace runs of slice(None) with Ellipsis. We specifically test if [0,...,0] is generated alongside [0,:,:,:,0] """ strat = xps.indices(shape=(1, 0, 0, 0, 1), max_dims=3, allow_ellipsis=True) find_any(strat, lambda ix: len(ix) == 3 and ix[1] == Ellipsis) find_any( strat, lambda ix: len(ix) == 5 and all(isinstance(key, slice) and key == slice(None) for key in ix[1:3]), ) def test_indices_replaces_whole_axis_slices_with_ellipsis(xp, xps): # `slice(None)` (aka `:`) is the only valid index for an axis of size # zero, so if all dimensions are 0 then a `...` will replace all the # slices because we generate `...` for entire contiguous runs of `:` assert_all_examples( xps.indices(shape=(0, 0, 0, 0, 0), max_dims=5).filter( lambda idx: isinstance(idx, tuple) and Ellipsis in idx ), lambda idx: slice(None) not in idx, ) def test_efficiently_generate_indexers(xp, xps): """Generation is not too slow.""" find_any(xps.indices((3, 3, 3, 3, 3))) @given(allow_newaxis=st.booleans(), allow_ellipsis=st.booleans(), data=st.data()) def test_generate_valid_indices(xp, xps, allow_newaxis, allow_ellipsis, data): """Strategy generates valid indices.""" shape = data.draw( xps.array_shapes(min_dims=1, max_side=4) | xps.array_shapes(min_dims=1, min_side=0, max_side=10), label="shape", ) min_dims = data.draw( st.integers(0, len(shape) if not allow_newaxis else len(shape) + 2), label="min_dims", ) max_dims = data.draw( st.none() | st.integers(min_dims, len(shape) if not allow_newaxis else NDIM_MAX), label="max_dims", ) indexer = data.draw( xps.indices( shape, min_dims=min_dims, max_dims=max_dims, allow_newaxis=allow_newaxis, allow_ellipsis=allow_ellipsis, ), label="indexer", ) _indexer = indexer if isinstance(indexer, tuple) else (indexer,) # Check that disallowed things are indeed absent if not allow_ellipsis: assert Ellipsis not in _indexer if not allow_newaxis: assert None not in _indexer # i.e. xp.newaxis # Check index is composed of valid objects for i in _indexer: assert isinstance(i, (int, slice)) or i is None or i == Ellipsis # Check indexer does not flat index nonexpanding_indexer = [i for i in _indexer if i is not None] if Ellipsis in _indexer: assert sum(i == Ellipsis for i in _indexer) == 1 # Note ellipsis can index 0 axes assert len(nonexpanding_indexer) <= len(shape) + 1 else: assert len(nonexpanding_indexer) == len(shape) if 0 in shape: # If there's a zero in the shape, the array will have no elements. array = xp.zeros(shape) assert array.size == 0 # sanity check elif math.prod(shape) <= 10**5: # If it's small enough to instantiate, do so with distinct elements. array = xp.reshape(xp.arange(math.prod(shape)), shape) else: # We can't cheat on this one, so just try another. assume(False) # Finally, check that we can use our indexer without error note(f"{array=}") array[indexer] ================================================ FILE: hypothesis-python/tests/array_api/test_partial_adoptors.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import functools import warnings from copy import copy from types import SimpleNamespace import pytest from hypothesis import given, strategies as st from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.extra.array_api import ( COMPLEX_NAMES, DTYPE_NAMES, FLOAT_NAMES, INT_NAMES, UINT_NAMES, make_strategies_namespace, mock_xp, ) from tests.common.debug import check_can_generate_examples MOCK_WARN_MSG = f"determine.*{mock_xp.__name__}.*Array API" class MockedArray: def __init__(self, wrapped, *, exclude=()): self.wrapped = wrapped self.exclude = exclude def __getattr__(self, name): if name in self.exclude: raise AttributeError(f"removed on the mock: {name}") return object.__getattr__(self, name) def wrap_array(func: callable, exclude: tuple[str, ...] = ()) -> callable: @functools.wraps(func) def wrapped(*args, **kwargs): result = func(*args, **kwargs) if isinstance(result, tuple): return tuple(MockedArray(arr, exclude=exclude) for arr in result) return MockedArray(result, exclude=exclude) return wrapped def make_mock_xp( *, exclude: tuple[str, ...] = (), exclude_methods: tuple[str, ...] = () ) -> SimpleNamespace: xp = copy(mock_xp) assert isinstance(exclude, tuple) # sanity check assert isinstance(exclude_methods, tuple) # sanity check for attr in exclude: delattr(xp, attr) array_returning_funcs = ( "astype", "broadcast_arrays", "arange", "asarray", "empty", "zeros", "ones", "reshape", "isnan", "isfinite", "logical_or", "sum", "nonzero", "sort", "unique_values", "any", "all", ) for name in array_returning_funcs: func = getattr(xp, name, None) if func is None: # removed in the step before continue setattr(xp, name, wrap_array(func, exclude=exclude_methods)) return xp def test_warning_on_noncompliant_xp(): """Using non-compliant array modules raises helpful warning""" xp = make_mock_xp(exclude_methods=("__array_namespace__",)) with pytest.warns(HypothesisWarning, match=MOCK_WARN_MSG): make_strategies_namespace(xp, api_version="draft") @pytest.mark.filterwarnings(f"ignore:.*{MOCK_WARN_MSG}.*") @pytest.mark.parametrize( "stratname, args, attr", [("from_dtype", ["int8"], "iinfo"), ("arrays", ["int8", 5], "asarray")], ) def test_error_on_missing_attr(stratname, args, attr): """Strategies raise helpful error when using array modules that lack required attributes.""" xp = make_mock_xp(exclude=(attr,)) xps = make_strategies_namespace(xp, api_version="draft") func = getattr(xps, stratname) with pytest.raises(InvalidArgument, match=f"{mock_xp.__name__}.*required.*{attr}"): check_can_generate_examples(func(*args)) dtypeless_xp = make_mock_xp(exclude=tuple(DTYPE_NAMES)) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=HypothesisWarning) dtypeless_xps = make_strategies_namespace(dtypeless_xp, api_version="draft") @pytest.mark.parametrize( "stratname", [ "scalar_dtypes", "boolean_dtypes", "numeric_dtypes", "integer_dtypes", "unsigned_integer_dtypes", "floating_dtypes", "real_dtypes", "complex_dtypes", ], ) def test_error_on_missing_dtypes(stratname): """Strategies raise helpful error when using array modules that lack required dtypes.""" func = getattr(dtypeless_xps, stratname) with pytest.raises(InvalidArgument, match=f"{mock_xp.__name__}.*dtype.*namespace"): check_can_generate_examples(func()) @pytest.mark.filterwarnings(f"ignore:.*{MOCK_WARN_MSG}.*") @pytest.mark.parametrize( "stratname, keep_anys", [ ("scalar_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES]), ("numeric_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES, COMPLEX_NAMES]), ("integer_dtypes", [INT_NAMES]), ("unsigned_integer_dtypes", [UINT_NAMES]), ("floating_dtypes", [FLOAT_NAMES]), ("real_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES]), ("complex_dtypes", [COMPLEX_NAMES]), ], ) @given(st.data()) def test_warning_on_partial_dtypes(stratname, keep_anys, data): """Strategies using array modules with at least one of a dtype in the necessary category/categories execute with a warning.""" exclude = [] for keep_any in keep_anys: exclude.extend( data.draw( st.lists( st.sampled_from(keep_any), min_size=1, max_size=len(keep_any) - 1, unique=True, ) ) ) xp = make_mock_xp(exclude=tuple(exclude)) xps = make_strategies_namespace(xp, api_version="draft") func = getattr(xps, stratname) with pytest.warns(HypothesisWarning, match=f"{mock_xp.__name__}.*dtype.*namespace"): data.draw(func()) def test_raises_on_inferring_with_no_dunder_version(): """When xp has no __array_api_version__, inferring api_version raises helpful error.""" xp = make_mock_xp(exclude=("__array_api_version__",)) with pytest.raises(InvalidArgument, match="has no attribute"): make_strategies_namespace(xp) def test_raises_on_invalid_dunder_version(): """When xp has invalid __array_api_version__, inferring api_version raises helpful error.""" xp = make_mock_xp() xp.__array_api_version__ = None with pytest.raises(InvalidArgument): make_strategies_namespace(xp) ================================================ FILE: hypothesis-python/tests/array_api/test_pretty.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from inspect import signature import pytest from hypothesis.errors import InvalidArgument from hypothesis.extra import array_api from hypothesis.extra.array_api import make_strategies_namespace from tests.array_api.common import MIN_VER_FOR_COMPLEX @pytest.mark.parametrize( "name", [ "from_dtype", "arrays", "array_shapes", "scalar_dtypes", "boolean_dtypes", "numeric_dtypes", "integer_dtypes", "unsigned_integer_dtypes", "floating_dtypes", "real_dtypes", pytest.param( "complex_dtypes", marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) ), "valid_tuple_axes", "broadcastable_shapes", "mutually_broadcastable_shapes", "indices", ], ) def test_namespaced_methods_meta(xp, xps, name): """Namespaced method objects have good meta attributes.""" func = getattr(xps, name) assert func.__name__ == name assert func.__doc__ is not None # The (private) top-level strategy methods may expose a xp argument in their # function signatures. make_strategies_namespace() exists to wrap these # top-level methods by binding the passed xp argument, and so the namespace # it returns should not expose xp in any of its function signatures. assert "xp" not in signature(func).parameters @pytest.mark.parametrize( "name, valid_args", [ ("from_dtype", ["int8"]), ("arrays", ["int8", 5]), ("array_shapes", []), ("scalar_dtypes", []), ("boolean_dtypes", []), ("numeric_dtypes", []), ("integer_dtypes", []), ("unsigned_integer_dtypes", []), ("floating_dtypes", []), ("real_dtypes", []), pytest.param( "complex_dtypes", [], marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) ), ("valid_tuple_axes", [0]), ("broadcastable_shapes", [()]), ("mutually_broadcastable_shapes", [3]), ("indices", [(5,)]), ], ) def test_namespaced_strategies_repr(xp, xps, name, valid_args): """Namespaced strategies have good repr.""" func = getattr(xps, name) strat = func(*valid_args) assert repr(strat).startswith(name + "("), f"{name} not in strat repr {strat!r}" assert len(repr(strat)) < 100, "strat repr looks too long" assert xp.__name__ not in repr(strat), f"{xp.__name__} in strat repr" @pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") def test_inferred_version_strategies_namespace_repr(xp): """Strategies namespace has good repr when api_version=None.""" try: xps = make_strategies_namespace(xp) except InvalidArgument as e: pytest.skip(str(e)) expected = f"make_strategies_namespace({xp.__name__})" assert repr(xps) == expected assert str(xps) == expected @pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") def test_specified_version_strategies_namespace_repr(xp, monkeypatch): """Strategies namespace has good repr when api_version is specified.""" monkeypatch.setattr(array_api, "_args_to_xps", {}) # ignore cached versions xps = make_strategies_namespace(xp, api_version="2021.12") expected = f"make_strategies_namespace({xp.__name__}, api_version='2021.12')" assert repr(xps) == expected assert str(xps) == expected ================================================ FILE: hypothesis-python/tests/array_api/test_scalar_dtypes.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis.extra.array_api import ( COMPLEX_NAMES, DTYPE_NAMES, FLOAT_NAMES, INT_NAMES, NUMERIC_NAMES, REAL_NAMES, UINT_NAMES, ) from tests.array_api.common import MIN_VER_FOR_COMPLEX from tests.common.debug import assert_all_examples, find_any, minimal @pytest.mark.parametrize( ("strat_name", "dtype_names"), [ ("integer_dtypes", INT_NAMES), ("unsigned_integer_dtypes", UINT_NAMES), ("floating_dtypes", FLOAT_NAMES), ("real_dtypes", REAL_NAMES), pytest.param( "complex_dtypes", COMPLEX_NAMES, marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX), ), ], ) def test_all_generated_dtypes_are_of_group(xp, xps, strat_name, dtype_names): """Strategy only generates expected dtypes.""" strat_func = getattr(xps, strat_name) dtypes = [getattr(xp, n) for n in dtype_names] assert_all_examples(strat_func(), lambda dtype: dtype in dtypes) def test_all_generated_scalar_dtypes_are_scalar(xp, xps): """Strategy only generates scalar dtypes.""" if xps.api_version > "2021.12": dtypes = [getattr(xp, n) for n in DTYPE_NAMES] else: dtypes = [getattr(xp, n) for n in ("bool", *REAL_NAMES)] assert_all_examples(xps.scalar_dtypes(), lambda dtype: dtype in dtypes) def test_all_generated_numeric_dtypes_are_numeric(xp, xps): """Strategy only generates numeric dtypes.""" if xps.api_version > "2021.12": dtypes = [getattr(xp, n) for n in NUMERIC_NAMES] else: dtypes = [getattr(xp, n) for n in REAL_NAMES] assert_all_examples(xps.numeric_dtypes(), lambda dtype: dtype in dtypes) def skipif_unsupported_complex(strat_name, dtype_name): if not dtype_name.startswith("complex"): return strat_name, dtype_name mark = pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) return pytest.param(strat_name, dtype_name, marks=mark) @pytest.mark.parametrize( ("strat_name", "dtype_name"), [ *[skipif_unsupported_complex("scalar_dtypes", n) for n in DTYPE_NAMES], *[skipif_unsupported_complex("numeric_dtypes", n) for n in NUMERIC_NAMES], *[("integer_dtypes", n) for n in INT_NAMES], *[("unsigned_integer_dtypes", n) for n in UINT_NAMES], *[("floating_dtypes", n) for n in FLOAT_NAMES], *[("real_dtypes", n) for n in REAL_NAMES], *[skipif_unsupported_complex("complex_dtypes", n) for n in COMPLEX_NAMES], ], ) def test_strategy_can_generate_every_dtype(xp, xps, strat_name, dtype_name): """Strategy generates every expected dtype.""" strat_func = getattr(xps, strat_name) dtype = getattr(xp, dtype_name) find_any(strat_func(), lambda d: d == dtype) def test_minimise_scalar_dtypes(xp, xps): """Strategy minimizes to bool dtype.""" assert minimal(xps.scalar_dtypes()) == xp.bool @pytest.mark.parametrize( "strat_name, sizes", [ ("integer_dtypes", 8), ("unsigned_integer_dtypes", 8), ("floating_dtypes", 32), pytest.param( "complex_dtypes", 64, marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) ), ], ) def test_can_specify_sizes_as_an_int(xp, xps, strat_name, sizes): """Strategy treats ints as a single size.""" strat_func = getattr(xps, strat_name) strat = strat_func(sizes=sizes) find_any(strat) ================================================ FILE: hypothesis-python/tests/array_api/test_strategies_namespace.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from types import SimpleNamespace from weakref import WeakValueDictionary import pytest from hypothesis.control import settings from hypothesis.extra import array_api from hypothesis.extra.array_api import ( NOMINAL_VERSIONS, make_strategies_namespace, mock_xp, ) from hypothesis.strategies import SearchStrategy pytestmark = [ pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning"), pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason="make_strategies_namespace is not thread safe", ), ] class HashableArrayModuleFactory: """ mock_xp cannot be hashed and thus cannot be used in our cache. So just for the purposes of testing the cache, we wrap it with an unsafe hash method. """ def __getattr__(self, name): return getattr(mock_xp, name) def __hash__(self): return hash(tuple(sorted(mock_xp.__dict__))) @pytest.mark.parametrize("api_version", ["2021.12", None]) def test_caching(api_version, monkeypatch): """Caches namespaces respective to arguments.""" xp = HashableArrayModuleFactory() assert isinstance(array_api._args_to_xps, WeakValueDictionary) # sanity check monkeypatch.setattr(array_api, "_args_to_xps", WeakValueDictionary()) assert len(array_api._args_to_xps) == 0 # sanity check xps1 = array_api.make_strategies_namespace(xp, api_version=api_version) assert len(array_api._args_to_xps) == 1 xps2 = array_api.make_strategies_namespace(xp, api_version=api_version) assert len(array_api._args_to_xps) == 1 assert isinstance(xps2, SimpleNamespace) assert xps2 is xps1 del xps1 del xps2 assert len(array_api._args_to_xps) == 0 @pytest.mark.parametrize( "api_version1, api_version2", [(None, "2021.12"), ("2021.12", None)] ) def test_inferred_namespace_shares_cache(api_version1, api_version2, monkeypatch): """Results from inferred versions share the same cache key as results from specified versions.""" xp = HashableArrayModuleFactory() xp.__array_api_version__ = "2021.12" assert isinstance(array_api._args_to_xps, WeakValueDictionary) # sanity check monkeypatch.setattr(array_api, "_args_to_xps", WeakValueDictionary()) assert len(array_api._args_to_xps) == 0 # sanity check xps1 = array_api.make_strategies_namespace(xp, api_version=api_version1) assert xps1.api_version == "2021.12" # sanity check assert len(array_api._args_to_xps) == 1 xps2 = array_api.make_strategies_namespace(xp, api_version=api_version2) assert xps2.api_version == "2021.12" # sanity check assert len(array_api._args_to_xps) == 1 assert xps2 is xps1 def test_complex_dtypes_raises_on_2021_12(): """Accessing complex_dtypes() for 2021.12 strategy namespace raises helpful error, but accessing on future versions returns expected strategy.""" first_xps = make_strategies_namespace(mock_xp, api_version="2021.12") with pytest.raises(AttributeError, match="attempted to access"): first_xps.complex_dtypes() for api_version in NOMINAL_VERSIONS[1:]: xps = make_strategies_namespace(mock_xp, api_version=api_version) assert isinstance(xps.complex_dtypes(), SearchStrategy) ================================================ FILE: hypothesis-python/tests/attrs/test_attrs.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import warnings import attr from hypothesis import given, strategies as st from hypothesis.errors import SmallSearchSpaceWarning from hypothesis.strategies._internal.utils import to_jsonable from tests.common.debug import check_can_generate_examples def a_converter(x) -> int: return int(x) @attr.s class Inferrables: annot_converter = attr.ib(converter=a_converter) @given(st.builds(Inferrables)) def test_attrs_inference_builds(c): pass def test_attrs_inference_from_type(): s = st.from_type(Inferrables) with warnings.catch_warnings(): warnings.simplefilter("ignore", SmallSearchSpaceWarning) check_can_generate_examples(s) @attr.s class AttrsClass: n = attr.ib() def test_jsonable_attrs(): obj = AttrsClass(n=10) assert to_jsonable(obj, avoid_realization=False) == {"n": 10} def test_hypothesis_is_not_the_first_to_import_attrs(testdir): # We only import attrs if the user did so first. test_path = testdir.makepyfile( """ import os # don't load hypothesis plugins, which might transitively import attrs os.environ["HYPOTHESIS_NO_PLUGINS"] = "1" import sys assert "attrs" not in sys.modules from hypothesis import given, strategies as st assert "attrs" not in sys.modules @given(st.integers() | st.floats() | st.sampled_from(["a", "b"])) def test_no_attrs_import(x): assert "attrs" not in sys.modules """ ) # don't load pytest plugins, which might transitively import attrs result = testdir.runpytest(test_path, "--disable-plugin-autoload") result.assert_outcomes(passed=1, failed=0) ================================================ FILE: hypothesis-python/tests/attrs/test_inference.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import typing import attr import pytest from hypothesis import given, strategies as st from hypothesis.errors import ResolutionFailed from tests.common.debug import check_can_generate_examples # Union[A, B] is not equivalent to A | B until 3.14. We'll continue to test both # until then. # ruff: noqa: UP045, UP007 @attr.s class Inferrables: type_ = attr.ib(type=int) type_converter = attr.ib(converter=bool) validator_type = attr.ib(validator=attr.validators.instance_of(str)) validator_type_tuple = attr.ib(validator=attr.validators.instance_of((str, int))) validator_type_multiple = attr.ib( validator=[ attr.validators.instance_of(str), attr.validators.instance_of((str, int, bool)), ] ) validator_type_has_overlap = attr.ib( validator=[ attr.validators.instance_of(str), attr.validators.instance_of((str, list)), attr.validators.instance_of(object), ] ) validator_optional = attr.ib( validator=attr.validators.optional(lambda inst, atrib, val: float(val)) ) validator_in = attr.ib(validator=attr.validators.in_([1, 2, 3])) validator_in_multiple = attr.ib( validator=[attr.validators.in_(list(range(100))), attr.validators.in_([1, -1])] ) validator_in_multiple_strings = attr.ib( validator=[attr.validators.in_("abcd"), attr.validators.in_(["ab", "cd"])] ) typing_list = attr.ib(type=list[int]) typing_list_of_list = attr.ib(type=list[list[int]]) typing_dict = attr.ib(type=dict[str, int]) typing_optional = attr.ib(type=typing.Optional[bool]) typing_optional_new = attr.ib(type=bool | None) typing_union = attr.ib(type=typing.Union[str, int]) typing_union_new = attr.ib(type=str | int) has_default = attr.ib(default=0) has_default_factory = attr.ib(default=attr.Factory(list)) has_default_factory_takes_self = attr.ib( # uninferrable but has default default=attr.Factory(lambda _: [], takes_self=True) ) @attr.s class Required: a = attr.ib() @attr.s class UnhelpfulConverter: a = attr.ib(converter=lambda x: x) @given(st.builds(Inferrables, has_default=..., has_default_factory=...)) def test_attrs_inference_builds(c): pass @given(st.from_type(Inferrables)) def test_attrs_inference_from_type(c): pass @pytest.mark.parametrize("c", [Required, UnhelpfulConverter]) def test_cannot_infer(c): with pytest.raises(ResolutionFailed): check_can_generate_examples(st.builds(c)) def test_cannot_infer_takes_self(): with pytest.raises(ResolutionFailed): check_can_generate_examples( st.builds(Inferrables, has_default_factory_takes_self=...) ) @attr.s class HasPrivateAttribute: _x: int = attr.ib() @pytest.mark.parametrize("s", [st.just(42)]) def test_private_attribute(s): check_can_generate_examples(st.builds(HasPrivateAttribute, x=s)) def test_private_attribute_underscore_fails(): with pytest.raises(TypeError, match="unexpected keyword argument '_x'"): check_can_generate_examples(st.builds(HasPrivateAttribute, _x=st.just(42))) def test_private_attribute_underscore_infer_fails(): # this has a slightly different failure case, because it goes through # attrs-specific resolution logic. with pytest.raises( TypeError, match="Unexpected keyword argument _x for attrs class" ): check_can_generate_examples(st.builds(HasPrivateAttribute, _x=...)) @attr.s class HasAliasedAttribute: x: int = attr.ib(alias="crazyname") @pytest.mark.parametrize("s", [st.just(42)]) def test_aliased_attribute(s): check_can_generate_examples(st.builds(HasAliasedAttribute, crazyname=s)) ================================================ FILE: hypothesis-python/tests/attrs/test_pretty.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import attrs from hypothesis.vendor import pretty class ReprDetector: def _repr_pretty_(self, p, cycle): """Exercise the IPython callback interface.""" p.text("GOOD") def __repr__(self): return "BAD" @attrs.define class SomeAttrsClass: x: ReprDetector def test_pretty_prints_attrs_classes(): assert pretty.pretty(SomeAttrsClass(ReprDetector())) == "SomeAttrsClass(x=GOOD)" @attrs.define class SomeAttrsClassWithCustomPretty: def _repr_pretty_(self, p, cycle): """Exercise the IPython callback interface.""" p.text("I am a banana") def test_custom_pretty_print_method_overrides_field_printing(): assert pretty.pretty(SomeAttrsClassWithCustomPretty()) == "I am a banana" @attrs.define class SomeAttrsClassWithLotsOfFields: a: int b: int c: int d: int e: int f: int g: int h: int i: int j: int k: int l: int m: int n: int o: int p: int q: int r: int s: int def test_will_line_break_between_fields(): obj = SomeAttrsClassWithLotsOfFields( **{ at.name: 12345678900000000000000001 for at in SomeAttrsClassWithLotsOfFields.__attrs_attrs__ } ) assert "\n" in pretty.pretty(obj) @attrs.define class SomeDataClassWithNoFields: ... def test_prints_empty_dataclass_correctly(): assert pretty.pretty(SomeDataClassWithNoFields()) == "SomeDataClassWithNoFields()" @attrs.define class AttrsClassWithNoInitField: x: int y: int = attrs.field(init=False) def test_does_not_include_no_init_fields_in_attrs_printing(): record = AttrsClassWithNoInitField(x=1) assert pretty.pretty(record) == "AttrsClassWithNoInitField(x=1)" record.y = 1 assert pretty.pretty(record) == "AttrsClassWithNoInitField(x=1)" class Namespace: @attrs.define class A: x: int def test_includes_namespace_classes_in_pretty(): obj = Namespace.A(x=1) assert pretty.pretty(obj) == "Namespace.A(x=1)" ================================================ FILE: hypothesis-python/tests/codemods/test_codemod_cli.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import subprocess BEFORE = """ from hypothesis.strategies import complex_numbers, complex_numbers as cn complex_numbers(min_magnitude=None) # simple call to fix complex_numbers(min_magnitude=None, max_magnitude=1) # plus arg after complex_numbers(allow_infinity=False, min_magnitude=None) # plus arg before cn(min_magnitude=None) # imported as alias """ AFTER = BEFORE.replace("None", "0") _unchanged = """ complex_numbers(min_magnitude=1) # value OK class Foo: def complex_numbers(self, **kw): pass complex_numbers(min_magnitude=None) # defined in a different scope """ BEFORE += _unchanged AFTER += _unchanged del _unchanged def run(command, *, cwd=None, input=None): return subprocess.run( command, input=input, capture_output=True, shell=True, text=True, cwd=cwd, encoding="utf-8", ) def test_codemod_single_file(tmp_path): fname = tmp_path / "mycode.py" fname.write_text(BEFORE, encoding="utf-8") result = run("hypothesis codemod mycode.py", cwd=tmp_path) assert result.returncode == 0 assert fname.read_text(encoding="utf-8") == AFTER def test_codemod_multiple_files(tmp_path): # LibCST had some trouble with multiprocessing on Windows files = [tmp_path / "mycode1.py", tmp_path / "mycode2.py"] for f in files: f.write_text(BEFORE, encoding="utf-8") result = run("hypothesis codemod mycode1.py mycode2.py", cwd=tmp_path) assert result.returncode == 0 for f in files: assert f.read_text(encoding="utf-8") == AFTER def test_codemod_from_stdin(): result = run("hypothesis codemod -", input=BEFORE) assert result.returncode == 0 assert result.stdout.rstrip() == AFTER.rstrip() ================================================ FILE: hypothesis-python/tests/codemods/test_codemods.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from libcst.codemod import CodemodTest from hypothesis.extra import codemods def test_refactor_function_is_idempotent(): before = ( "from hypothesis.strategies import complex_numbers\n\n" "complex_numbers(None)\n" ) after = codemods.refactor(before) assert before.replace("None", "min_magnitude=0") == after assert codemods.refactor(after) == after class TestFixComplexMinMagnitude(CodemodTest): TRANSFORM = codemods.HypothesisFixComplexMinMagnitude def test_noop(self) -> None: before = """ from hypothesis.strategies import complex_numbers, complex_numbers as cn complex_numbers(min_magnitude=1) # value OK complex_numbers(max_magnitude=None) # different argument class Foo: def complex_numbers(self, **kw): pass complex_numbers(min_magnitude=None) # defined in a different scope """ self.assertCodemod(before=before, after=before) def test_substitution(self) -> None: before = """ from hypothesis.strategies import complex_numbers, complex_numbers as cn complex_numbers(min_magnitude=None) # simple call to fix complex_numbers(min_magnitude=None, max_magnitude=1) # plus arg after complex_numbers(allow_infinity=False, min_magnitude=None) # plus arg before cn(min_magnitude=None) # imported as alias """ self.assertCodemod(before=before, after=before.replace("None", "0")) class TestFixPositionalKeywonlyArgs(CodemodTest): TRANSFORM = codemods.HypothesisFixPositionalKeywonlyArgs def test_substitution(self) -> None: before = """ import hypothesis.strategies as st st.floats(0, 1, False, False, 32) st.fractions(0, 1, 9) """ after = """ import hypothesis.strategies as st st.floats(0, 1, allow_nan=False, allow_infinity=False, width=32) st.fractions(0, 1, max_denominator=9) """ self.assertCodemod(before=before, after=after) def test_noop_with_new_floats_kw(self) -> None: before = """ import hypothesis.strategies as st st.floats(0, 1, False, False, True, 32, False, False) # allow_subnormal=True """ self.assertCodemod(before=before, after=before) def test_noop_if_unsure(self) -> None: before = """ import random if random.getrandbits(1): from hypothesis import target from hypothesis.strategies import lists as sets def fractions(*args): pass else: from hypothesis import target from hypothesis.strategies import fractions, sets fractions(0, 1, 9) sets(None, 1) target(0, 'label') """ after = before.replace("'label'", "label='label'") self.assertCodemod(before=before, after=after) def test_stateful_rule_noop(self): # `rule()(lambda self: None)` is a call with a positional argument, and # so we need an additional check that the "func" node is a Name rather than # itself being a Call, lest we rewrite the outer instead of the inner. # (this may be an upstream bug in metadata processing) before = """ from hypothesis.stateful import RuleBasedStateMachine, rule class MultipleRulesSameFuncMachine(RuleBasedStateMachine): rule1 = rule()(lambda self: None) """ self.assertCodemod(before=before, after=before) def test_kwargs_noop(self): before = """ from hypothesis import target kwargs = {"observation": 1, "label": "foobar"} target(**kwargs) """ self.assertCodemod(before=before, after=before) def test_noop_with_too_many_arguments_passed(self) -> None: # If there are too many arguments, we should leave this alone to raise # TypeError on older versions instead of deleting the additional args. before = """ import hypothesis.strategies as st st.sets(st.integers(), 0, 1, True) """ self.assertCodemod(before=before, after=before) class TestHealthCheckAll(CodemodTest): TRANSFORM = codemods.HypothesisFixHealthCheckAll def test_noop_other_attributes(self): # Test that calls to other attributes of HealthCheck are not modified before = "result = HealthCheck.data_too_large" self.assertCodemod(before=before, after=before) def test_substitution(self) -> None: # Test that HealthCheck.all() is replaced with list(HealthCheck) before = "result = HealthCheck.all()" after = "result = list(HealthCheck)" # self.assertEqual(run_codemod(input_code), expected_code) self.assertCodemod(before=before, after=after) class TestFixCharactersArguments(CodemodTest): TRANSFORM = codemods.HypothesisFixCharactersArguments def test_substitution(self) -> None: for in_, out in codemods.HypothesisFixCharactersArguments._replacements.items(): before = f""" import hypothesis.strategies as st st.characters({in_}=...) """ self.assertCodemod(before=before, after=before.replace(in_, out)) def test_remove_redundant_exclude_categories(self) -> None: args = "blacklist_categories=OUT, whitelist_categories=IN" before = f""" import hypothesis.strategies as st st.characters({args}) """ self.assertCodemod(before=before, after=before.replace(args, "categories=IN")) ================================================ FILE: hypothesis-python/tests/common/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys from collections import namedtuple from hypothesis.strategies import ( binary, booleans, builds, complex_numbers, decimals, dictionaries, fixed_dictionaries, floats, fractions, frozensets, integers, just, lists, none, one_of, randoms, recursive, sampled_from, sets, text, tuples, ) from tests.common.debug import TIME_INCREMENT __all__ = ["TIME_INCREMENT", "standard_types"] OrderedPair = namedtuple("OrderedPair", ("left", "right")) ABC = namedtuple("ABC", ("a", "b", "c")) def abc(x, y, z): return builds(ABC, x, y, z) standard_types = [ lists(none(), max_size=0), tuples(), sets(none(), max_size=0), frozensets(none(), max_size=0), fixed_dictionaries({}), abc(booleans(), booleans(), booleans()), abc(booleans(), booleans(), integers()), fixed_dictionaries({"a": integers(), "b": booleans()}), dictionaries(booleans(), integers()), dictionaries(text(), booleans()), one_of(integers(), tuples(booleans())), sampled_from(range(10)), one_of(just("a"), just("b"), just("c")), sampled_from(("a", "b", "c")), integers(), integers(min_value=3), integers(min_value=(-(2**32)), max_value=(2**64)), floats(), floats(min_value=-2.0, max_value=3.0), floats(), floats(min_value=-2.0), floats(), floats(max_value=-0.0), floats(), floats(min_value=0.0), floats(min_value=3.14, max_value=3.14), text(), binary(), booleans(), tuples(booleans(), booleans()), frozensets(integers()), sets(frozensets(booleans())), complex_numbers(), fractions(), decimals(), lists(lists(booleans())), lists(floats(0.0, 0.0)), integers().flatmap( lambda right: integers(min_value=0).map( lambda length: OrderedPair(right - length, right) ) ), integers().flatmap(lambda v: lists(just(v))), integers().filter(lambda x: abs(x) > 100), floats(min_value=-sys.float_info.max, max_value=sys.float_info.max), none(), randoms(use_true_random=True), booleans().flatmap(lambda x: booleans() if x else complex_numbers()), recursive(base=booleans(), extend=lambda x: lists(x, max_size=3), max_leaves=10), ] ================================================ FILE: hypothesis-python/tests/common/arguments.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, settings from hypothesis.errors import InvalidArgument def e(a, *args, **kwargs): return (a, args, kwargs) def e_to_str(elt): f, args, kwargs = getattr(elt, "values", elt) bits = list(map(repr, args)) bits.extend(sorted(f"{k}={v!r}" for k, v in kwargs.items())) return "{}({})".format(f.__name__, ", ".join(bits)) def argument_validation_test(bad_args): @pytest.mark.parametrize( ("function", "args", "kwargs"), bad_args, ids=list(map(e_to_str, bad_args)) ) def test_raise_invalid_argument(function, args, kwargs): # some invalid argument tests may find multiple distinct invalid inputs, # which hypothesis raises as an exception group (and is not caught by # pytest.raises). @given(function(*args, **kwargs)) @settings(report_multiple_bugs=False) def test(x): pass with pytest.raises(InvalidArgument): test() return test_raise_invalid_argument ================================================ FILE: hypothesis-python/tests/common/costbounds.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.internal.conjecture.shrinking.common import find_integer FIND_INTEGER_COSTS = {} def find_integer_cost(n): try: return FIND_INTEGER_COSTS[n] except KeyError: pass cost = 0 def test(i): nonlocal cost cost += 1 return i <= n find_integer(test) return FIND_INTEGER_COSTS.setdefault(n, cost) ================================================ FILE: hypothesis-python/tests/common/debug.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from typing import TypeVar from unittest import SkipTest from hypothesis import HealthCheck, Phase, Verbosity, given, settings as Settings from hypothesis._settings import local_settings from hypothesis.control import _current_build_context from hypothesis.errors import NoSuchExample, Unsatisfiable from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies import SearchStrategy from tests.common.utils import no_shrink TIME_INCREMENT = 0.00001 T = TypeVar("T") # don't use hypothesis.errors.Found, which inherits from HypothesisException # and therefore has weird semantics around e.g. backend="crosshair". class Found(Exception): pass def minimal( definition: SearchStrategy[T], condition=lambda x: True, settings: Settings | None = None, ) -> T: from tests.conftest import in_shrinking_benchmark definition.validate() result = None def wrapped_condition(x): # This sure seems pointless, but `test_sum_of_pair` fails otherwise... return condition(x) if ( context := _current_build_context.value ) and context.data.provider.avoid_realization: raise SkipTest("`minimal()` helper not supported under symbolic execution") if settings is None: settings = Settings(max_examples=500, phases=(Phase.generate, Phase.shrink)) verbosity = settings.verbosity if verbosity == Verbosity.normal: verbosity = Verbosity.quiet @given(definition) @Settings( parent=settings, suppress_health_check=list(HealthCheck), report_multiple_bugs=False, # we derandomize in general to avoid flaky tests, but we do want to # measure this variation while benchmarking. derandomize=not in_shrinking_benchmark, database=None, verbosity=verbosity, ) def inner(x): if wrapped_condition(x): nonlocal result result = x raise Found try: inner() except Found: return result raise Unsatisfiable( f"Could not find any examples from {definition!r} that satisfied " f"{get_pretty_function_description(condition)}" ) def find_any(definition, condition=lambda _: True, settings=None): # If nested within an existing @given if context := _current_build_context.value: while True: if condition(s := context.data.draw(definition)): return s # If top-level settings = settings or Settings.default return minimal( definition, condition, settings=Settings( settings, phases=no_shrink, max_examples=max(1000, settings.max_examples) ), ) def assert_no_examples(strategy, condition=lambda _: True): try: assert_all_examples(strategy, lambda val: not condition(val)) except (Unsatisfiable, NoSuchExample): pass def assert_all_examples(strategy, predicate, settings=None): """Asserts that all examples of the given strategy match the predicate. :param strategy: Hypothesis strategy to check :param predicate: (callable) Predicate that takes example and returns bool """ if context := _current_build_context.value: with local_settings(Settings(parent=settings)): for _ in range(20): s = context.data.draw(strategy) msg = f"Found {s!r} using strategy {strategy} which does not match" assert predicate(s), msg else: @given(strategy) @Settings(parent=settings, database=None) def assert_examples(s): msg = f"Found {s!r} using strategy {strategy} which does not match" assert predicate(s), msg assert_examples() def assert_simple_property(strategy, predicate, settings=None): """Like assert_all_examples, intended as a self-documenting shortcut for simple constant properties (`is`, `isinstance`, `==`, ...) that can be adequately verified in just a few examples. For more thorough checking, use assert_all_examples. """ assert_all_examples( strategy, predicate, Settings( parent=settings, max_examples=15, suppress_health_check=list(HealthCheck), ), ) def check_can_generate_examples(strategy, settings=None): """Tries to generate a small number of examples from the strategy, to verify that it can do so without raising. Nothing is returned, it only checks that no error is raised. """ assert_simple_property( strategy, lambda _: True, settings=Settings( parent=settings, phases=(Phase.generate,), ), ) ================================================ FILE: hypothesis-python/tests/common/setup.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os from warnings import filterwarnings from hypothesis import ( HealthCheck, Phase, Verbosity, _settings as settings_module, settings, ) from hypothesis._settings import CI, default as default_settings, is_in_ci, not_set from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS from hypothesis.internal.coverage import IN_COVERAGE_TESTS def run(): filterwarnings("error") filterwarnings("ignore", category=ImportWarning) filterwarnings("ignore", category=FutureWarning, module="pandas._version") # See https://github.com/numpy/numpy/pull/432; still a thing as of 2022. filterwarnings("ignore", message="numpy.dtype size changed") filterwarnings("ignore", message="numpy.ufunc size changed") # See https://github.com/HypothesisWorks/hypothesis/issues/1674 filterwarnings( "ignore", message=( "The virtualenv distutils package at .+ appears to be in the " "same location as the system distutils?" ), category=UserWarning, ) # We do a smoke test here before we mess around with settings. for setting_name in settings_module.all_settings: # database value is dynamically calculated if setting_name == "database": continue value = getattr(settings(), setting_name) default_value = getattr(default_settings, setting_name) assert value == default_value or ( is_in_ci() and value == getattr(CI, setting_name) ), f"({value!r} == x.{setting_name}) != (s.{setting_name} == {default_value!r})" settings.register_profile( "default", settings( default_settings, max_examples=20 if IN_COVERAGE_TESTS else not_set, phases=list(Phase), # Dogfooding the explain phase ), ) settings.register_profile("speedy", max_examples=5) settings.register_profile("debug", verbosity=Verbosity.debug) settings.register_profile( "threading", # we would normally not disable the deadline here, but we disable the # _consistently_increment_time fixture under threading tests (since # monkeypatching is not thread safe), which leads to real deadline errors # that are normally masked by that fixture. deadline=None, ) if "crosshair" in AVAILABLE_PROVIDERS: settings.register_profile( "crosshair", # inherit from default profile, even on CI. See # https://github.com/HypothesisWorks/hypothesis/pull/4536#issuecomment-3366741772 settings.get_profile("default"), backend="crosshair", max_examples=20, deadline=None, suppress_health_check=(HealthCheck.too_slow, HealthCheck.filter_too_much), report_multiple_bugs=False, ) for backend in set(AVAILABLE_PROVIDERS) - {"hypothesis", "crosshair"}: settings.register_profile(backend, backend=backend) settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) ================================================ FILE: hypothesis-python/tests/common/strategies.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import time from hypothesis.strategies._internal import SearchStrategy class _Slow(SearchStrategy): def do_draw(self, data): time.sleep(1.01) data.draw_bytes(2, 2) SLOW = _Slow() class HardToShrink(SearchStrategy): def __init__(self): super().__init__() self.__last = None self.accepted = set() def do_draw(self, data): x = bytes(data.draw_integer(0, 255) for _ in range(100)) if x in self.accepted: return True ls = self.__last if ls is None: if all(x): self.__last = x self.accepted.add(x) return True else: return False diffs = [i for i in range(len(x)) if x[i] != ls[i]] if len(diffs) == 1: i = diffs[0] if x[i] + 1 == ls[i]: self.__last = x self.accepted.add(x) return True return False ================================================ FILE: hypothesis-python/tests/common/utils.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import contextlib import enum import math import sys import time import warnings from io import StringIO from threading import Barrier, Lock, RLock, Thread import pytest from pytest import mark from hypothesis import Phase, settings from hypothesis.errors import HypothesisDeprecationWarning from hypothesis.internal import observability from hypothesis.internal.floats import next_down from hypothesis.internal.observability import ( Observation, add_observability_callback, remove_observability_callback, ) from hypothesis.internal.reflection import get_pretty_function_description, proxies from hypothesis.reporting import default, with_reporter from hypothesis.strategies._internal.core import from_type, register_type_strategy from hypothesis.strategies._internal.types import _global_type_lookup # we need real time here, not monkeypatched for CI time_sleep = time.sleep skipif_emscripten = mark.skipif( sys.platform == "emscripten", reason="threads, processes, etc. are not available in the browser", ) no_shrink = tuple(set(settings.default.phases) - {Phase.shrink, Phase.explain}) def flaky(max_runs, min_passes): assert isinstance(max_runs, int) assert isinstance(min_passes, int) assert 0 < min_passes <= max_runs <= 50 # arbitrary cap def accept(func): @proxies(func) def inner(*args, **kwargs): runs = passes = 0 while passes < min_passes: runs += 1 try: func(*args, **kwargs) passes += 1 except BaseException: if runs >= max_runs: raise return inner return accept capture_out_lock = Lock() @contextlib.contextmanager def capture_out(): # replacing the singleton sys.stdout can't be made thread safe. Disallow # concurrency by wrapping a lock around the entire block with capture_out_lock: old_out = sys.stdout try: new_out = StringIO() sys.stdout = new_out with with_reporter(default): yield new_out finally: sys.stdout = old_out class ExcInfo: pass def fails_with(e, *, match=None): def accepts(f): @proxies(f) def inverted_test(*arguments, **kwargs): with pytest.raises(e, match=match): f(*arguments, **kwargs) return inverted_test return accepts fails = fails_with(AssertionError) class NotDeprecated(Exception): pass @contextlib.contextmanager def validate_deprecation(): if settings.get_current_profile_name() == "threading": import pytest if sys.version_info[:2] < (3, 14): pytest.skip("warnings module is not thread-safe before 3.14") import warnings try: warnings.simplefilter("always", HypothesisDeprecationWarning) with warnings.catch_warnings(record=True) as w: yield finally: warnings.simplefilter("error", HypothesisDeprecationWarning) if not any(e.category == HypothesisDeprecationWarning for e in w): raise NotDeprecated( f"Expected a deprecation warning but got {[e.category for e in w]!r}" ) def checks_deprecated_behaviour(func): """A decorator for testing deprecated behaviour.""" @proxies(func) def _inner(*args, **kwargs): with validate_deprecation(): return func(*args, **kwargs) return _inner def all_values(db): return {v for vs in db.data.values() for v in vs} def non_covering_examples(database): return { v for k, vs in database.data.items() if not k.endswith(b".pareto") for v in vs } def counts_calls(func): """A decorator that counts how many times a function was called, and stores that value in a ``.calls`` attribute. """ assert not hasattr(func, "calls") @proxies(func) def _inner(*args, **kwargs): _inner.calls += 1 return func(*args, **kwargs) _inner.calls = 0 return _inner def assert_output_contains_failure(output, test, **kwargs): assert test.__name__ + "(" in output for k, v in kwargs.items(): assert f"{k}={v!r}" in output, (f"{k}={v!r}", output) def assert_falsifying_output( test, example_type="Falsifying", expected_exception=AssertionError, **kwargs ): with capture_out() as out: if expected_exception is None: # Some tests want to check the output of non-failing runs. test() msg = "" else: with pytest.raises(expected_exception) as exc_info: test() notes = "\n".join(getattr(exc_info.value, "__notes__", [])) msg = str(exc_info.value) + "\n" + notes output = out.getvalue() + msg assert f"{example_type} example:" in output assert_output_contains_failure(output, test, **kwargs) temp_registered_lock = RLock() @contextlib.contextmanager def temp_registered(type_, strat_or_factory): """Register and un-register a type for st.from_type(). This is not too hard, but there's a subtlety in restoring the previously-registered strategy which we got wrong in a few places. """ with temp_registered_lock: prev = _global_type_lookup.get(type_) register_type_strategy(type_, strat_or_factory) try: yield finally: del _global_type_lookup[type_] from_type.__clear_cache() if prev is not None: register_type_strategy(type_, prev) @contextlib.contextmanager def raises_warning(expected_warning, match=None): """Use instead of pytest.warns to check that the raised warning is handled properly""" with pytest.raises(expected_warning, match=match) as r, warnings.catch_warnings(): warnings.simplefilter("error", category=expected_warning) yield r @contextlib.contextmanager def capture_observations(*, choices=None): ls: list[Observation] = [] add_observability_callback(ls.append) if choices is not None: old_choices = observability.OBSERVABILITY_CHOICES observability.OBSERVABILITY_CHOICES = choices try: yield ls finally: remove_observability_callback(ls.append) if choices is not None: observability.OBSERVABILITY_CHOICES = old_choices # Specifies whether we can represent subnormal floating point numbers. # IEE-754 requires subnormal support, but it's often disabled anyway by unsafe # compiler options like `-ffast-math`. On most hardware that's even a global # config option, so *linking against* something built this way can break us. # Everything is terrible PYTHON_FTZ = next_down(sys.float_info.min) == 0.0 class Why(enum.Enum): # Categorizing known failures, to ease later follow-up investigation. # Some are crosshair issues, some hypothesis issues, others truly ok-to-xfail tests. symbolic_outside_context = "CrosshairInternal error (using value outside context)" nested_given = "nested @given decorators don't work with crosshair" undiscovered = "crosshair may not find the failing input" other = "reasons not elsewhere categorized" def xfail_on_crosshair(why: Why, /, *, strict=True, as_marks=False): # run `pytest -m xf_crosshair` to select these tests! mark = pytest.mark.xfail( strict=strict and why != Why.undiscovered, reason=f"Expected failure due to: {why.value}", condition=settings().backend == "crosshair", ) if as_marks: # for use with pytest.param(..., marks=xfail_on_crosshair()) return (pytest.mark.xf_crosshair, mark) return lambda fn: pytest.mark.xf_crosshair(mark(fn)) def skipif_threading(f): return pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason="not thread safe" )(f) def xfail_if_gil_disabled(f): try: if not sys._is_gil_enabled(): # 3.13+ return pytest.mark.xfail( reason="fails on free-threading build", strict=False )(f) except Exception: pass return f # we don't monkeypatch _consistently_increment_time under threading skipif_time_unpatched = skipif_threading _restore_recursion_limit_lock = RLock() @contextlib.contextmanager def restore_recursion_limit(): with _restore_recursion_limit_lock: original_limit = sys.getrecursionlimit() try: yield finally: sys.setrecursionlimit(original_limit) def run_concurrently(function, *, n: int) -> None: import pytest if settings.get_current_profile_name() == "crosshair": pytest.skip("crosshair is not thread safe") if sys.platform == "emscripten": pytest.skip("no threads on emscripten") def run(): barrier.wait() function() threads = [Thread(target=run) for _ in range(n)] barrier = Barrier(n) for thread in threads: thread.start() for thread in threads: thread.join(timeout=10) def wait_for(condition, *, timeout=1, interval=0.01): for _ in range(math.ceil(timeout / interval)): if condition(): return time_sleep(interval) raise Exception( f"timing out after waiting {timeout}s for condition " f"{get_pretty_function_description(condition)}" ) ================================================ FILE: hypothesis-python/tests/conftest.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import gc import inspect import json import os import random import sys import time as time_module from functools import wraps from pathlib import Path import pytest from _pytest.monkeypatch import MonkeyPatch from hypothesis import is_hypothesis_test, settings from hypothesis._settings import is_in_ci from hypothesis.errors import NonInteractiveExampleWarning from hypothesis.internal import lambda_sources from hypothesis.internal.compat import add_note from hypothesis.internal.conjecture import junkdrawer from tests.common import TIME_INCREMENT from tests.common.setup import run from tests.common.utils import raises_warning run() # Skip collection of tests which require the Django test runner, # or that don't work on the current version of Python. collect_ignore_glob = ["django/*"] # this checks up until py319 for minor_increment in range(10): minor = 10 + minor_increment if sys.version_info < (3, minor): collect_ignore_glob.append(f"cover/*py3{minor}*.py") if sys.version_info >= (3, 11): collect_ignore_glob.append("cover/test_asyncio.py") # @asyncio.coroutine removed in_shrinking_benchmark = False def pytest_configure(config): config.addinivalue_line("markers", "slow: pandas expects this marker to exist.") config.addinivalue_line( "markers", "xp_min_version(api_version): run when greater or equal to api_version", ) config.addinivalue_line("markers", "xf_crosshair: selection for xfailing symbolics") if config.getoption("--hypothesis-benchmark-shrinks"): # we'd like to support xdist here, but a session-scope fixture won't # be enough: https://github.com/pytest-dev/pytest-xdist/issues/271. # Need a lockfile or equivalent. assert config.getoption( "--hypothesis-benchmark-output" ), "must specify shrinking output file" global in_shrinking_benchmark in_shrinking_benchmark = True def pytest_addoption(parser): parser.addoption("--hypothesis-update-outputs", action="store_true") parser.addoption("--hypothesis-benchmark-shrinks", type=str, choices=["new", "old"]) parser.addoption("--hypothesis-benchmark-output", type=str) @pytest.fixture(scope="function", params=["warns", "raises"]) def warns_or_raises(request): """This runs the test twice: first to check that a warning is emitted and execution continues successfully despite the warning; then to check that the raised warning is handled properly. """ if request.param == "raises": return raises_warning else: return pytest.warns # crosshair needs actual time for its path timeouts; load it before patching try: import hypothesis_crosshair_provider.crosshair_provider # noqa: F401 except ImportError: pass if sys.version_info >= (3, 11): # To detect if changes in code generation causes lambda test compilation # to fail. Older versions (3.10 and earlier) have a few known false # negatives which we ignore. @pytest.fixture(scope="function", autouse=True) def _make_unknown_lambdas_fail(monkeypatch): def fail(candidate): msg = ( f"Failed to find a matching source for {candidate}. " "This could indicate changes in the Python code generator,\n" "or just a previously unknown case. To quickly resolve this " "problem, use the `allow_unknown_lambdas` fixture." ) raise AssertionError(msg) monkeypatch.setattr( lambda_sources, "_check_unknown_perfectly_aligned_lambda", fail ) @pytest.fixture(scope="function") def allow_unknown_lambdas(monkeypatch): # Will run after make...fail since autouse are run first def nofail(candidate): pass monkeypatch.setattr( lambda_sources, "_check_unknown_perfectly_aligned_lambda", nofail ) # monkeypatch is not thread-safe, so pytest-run-parallel will skip all our tests # if we define this. if settings.get_current_profile_name() != "threading": @pytest.fixture(scope="function", autouse=True) def _consistently_increment_time(monkeypatch): """Rather than rely on real system time we monkey patch time.time so that it passes at a consistent rate between calls. The reason for this is that when these tests run in CI, their performance is extremely variable and the VM the tests are on might go to sleep for a bit, introducing arbitrary delays. This can cause a number of tests to fail flakily. Replacing time with a fake version under our control avoids this problem. """ frozen = False current_time = time_module.time() def time(): nonlocal current_time if not frozen: current_time += TIME_INCREMENT return current_time def sleep(naptime): nonlocal current_time current_time += naptime def freeze(): nonlocal frozen frozen = True def _patch(name, fn): monkeypatch.setattr( time_module, name, wraps(getattr(time_module, name))(fn) ) _patch("time", time) _patch("monotonic", time) _patch("perf_counter", time) _patch("sleep", sleep) monkeypatch.setattr(time_module, "freeze", freeze, raising=False) # In the patched time regime, observing it causes it to increment. To avoid reintroducing # non-determinism due to GC running at arbitrary times, we patch the GC observer # to NOT increment time. monkeypatch.setattr(junkdrawer, "_perf_counter", time) if hasattr(gc, "callbacks"): # ensure timer callback is added, then bracket it by freeze/unfreeze below junkdrawer.gc_cumulative_time() _was_frozen = False def _freezer(*_): nonlocal _was_frozen, frozen _was_frozen = frozen frozen = True def _unfreezer(*_): nonlocal _was_frozen, frozen frozen = _was_frozen gc.callbacks.insert(0, _freezer) # freeze before gc callback gc.callbacks.append(_unfreezer) # unfreeze after yield assert gc.callbacks.pop(0) == _freezer assert gc.callbacks.pop() == _unfreezer else: # pragma: no cover # branch never taken in CPython yield random_states_after_tests = {} independent_random = random.Random() @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): if item.config.getoption("--hypothesis-benchmark-shrinks"): yield from _benchmark_shrinks(item) # ideally benchmark shrinking would not be mutually exclusive with the # other checks in this function, but it's cleaner to early-return here, # and in practice they will error in normal tests before one runs a # benchmark. return # This hookwrapper checks for PRNG state leaks from Hypothesis tests. # See: https://github.com/HypothesisWorks/hypothesis/issues/1919 if ( not (hasattr(item, "obj") and is_hypothesis_test(item.obj)) # we disable this check on the threading job, due to races in the global # state. or settings.get_current_profile_name() == "threading" ): outcome = yield elif "pytest_randomly" in sys.modules: # See https://github.com/HypothesisWorks/hypothesis/issues/3041 - this # branch exists to make it easier on external contributors, but should # never run in our CI (because that would disable the check entirely). assert not is_in_ci() outcome = yield else: # We start by peturbing the state of the PRNG, because repeatedly # leaking PRNG state resets state_after to the (previously leaked) # state_before, and that just shows as "no use of random". random.seed(independent_random.randrange(2**32)) before = random.getstate() outcome = yield after = random.getstate() if before != after: if after in random_states_after_tests: raise Exception( f"{item.nodeid!r} and {random_states_after_tests[after]!r} " "both used the `random` module, and finished with the " "same global `random.getstate()`; this is probably a nasty bug!" ) random_states_after_tests[after] = item.nodeid # Annotate usage of .example() with a hint about alternatives if isinstance(getattr(outcome, "exception", None), NonInteractiveExampleWarning): add_note( outcome.exception, "For hypothesis' own test suite, consider using one of the helper " "methods in tests.common.debug instead.", ) timer = time_module.process_time def _worker_path(session: pytest.Session) -> Path: return ( Path(session.config.getoption("--hypothesis-benchmark-output")).parent # https://pytest-xdist.readthedocs.io/en/stable/how-to.html#envvar-PYTEST_XDIST_WORKER / f"shrinking_results_{os.environ['PYTEST_XDIST_WORKER']}.json" ) def _benchmark_shrinks(item: pytest.Function) -> None: from hypothesis.internal.conjecture.shrinker import Shrinker # this isn't perfect, but it is cheap! if "minimal(" not in inspect.getsource(item.function): pytest.skip("(probably) does not call minimal()") actual_shrink = Shrinker.shrink shrink_calls = [] shrink_time = [] def shrink(self, *args, **kwargs): nonlocal shrink_calls nonlocal shrink_time start_t = timer() result = actual_shrink(self, *args, **kwargs) shrink_calls.append(self.engine.call_count - self.initial_calls) shrink_time.append(timer() - start_t) return result monkeypatch = MonkeyPatch() monkeypatch.setattr(Shrinker, "shrink", shrink) # pytest_runtest_call must yield at some point. This executes the test function # n - 1 times, and then yields for the final execution. for _ in range(5 - 1): item.runtest() yield monkeypatch.undo() # remove leading hypothesis-python/tests/... nodeid = item.nodeid.rsplit("/", 1)[1] results_p = _worker_path(item.session) if not results_p.exists(): results_p.write_text(json.dumps({"calls": {}, "time": {}})) data = json.loads(results_p.read_text()) data["calls"][nodeid] = shrink_calls data["time"][nodeid] = shrink_time results_p.write_text(json.dumps(data)) def pytest_sessionfinish(session, exitstatus): if not (mode := session.config.getoption("--hypothesis-benchmark-shrinks")): return # only run on the controller process, not the workers if hasattr(session.config, "workerinput"): return results = {"calls": {}, "time": {}} output_p = Path(session.config.getoption("--hypothesis-benchmark-output")) for p in output_p.parent.iterdir(): if p.name.startswith("shrinking_results_"): worker_results = json.loads(p.read_text()) results["calls"] |= worker_results["calls"] results["time"] |= worker_results["time"] p.unlink() results = {mode: results} if not output_p.exists(): output_p.write_text(json.dumps(results)) else: data = json.loads(output_p.read_text()) data[mode] = results[mode] output_p.write_text(json.dumps(data)) ================================================ FILE: hypothesis-python/tests/conjecture/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/conjecture/common.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import dataclasses import math import sys from contextlib import contextmanager from random import Random from threading import RLock import pytest from hypothesis import HealthCheck, Phase, settings, strategies as st from hypothesis.control import current_build_context, currently_in_test_context from hypothesis.internal.conjecture import engine as engine_module from hypothesis.internal.conjecture.choice import ChoiceNode, ChoiceT from hypothesis.internal.conjecture.data import ConjectureData, Status from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.conjecture.provider_conformance import ( choice_types_constraints, constraints_strategy, ) from hypothesis.internal.conjecture.providers import COLLECTION_DEFAULT_MAX_SIZE from hypothesis.internal.conjecture.utils import calc_label_from_name from hypothesis.internal.escalation import InterestingOrigin from hypothesis.internal.floats import SMALLEST_SUBNORMAL from hypothesis.internal.intervalsets import IntervalSet SOME_LABEL = calc_label_from_name("some label") def interesting_origin(n: int | None = None) -> InterestingOrigin: """ Creates and returns an InterestingOrigin, parameterized by n, such that interesting_origin(n) == interesting_origin(m) iff n = m. Since n=None may by chance concide with an explicitly-passed value of n, I recommend not mixing interesting_origin() and interesting_origin(n) in the same test. """ try: int("not an int") except Exception as e: origin = InterestingOrigin.from_exception(e) return dataclasses.replace(origin, lineno=n if n is not None else origin.lineno) def run_to_data(f): runner = ConjectureRunner( f, settings=settings( max_examples=300, database=None, suppress_health_check=list(HealthCheck) ), random=Random(0), ) runner.run() assert runner.interesting_examples (last_data,) = runner.interesting_examples.values() return last_data def run_to_nodes(f): return run_to_data(f).nodes _buffer_size_lock = RLock() @contextmanager def buffer_size_limit(n): with _buffer_size_lock: original = engine_module.BUFFER_SIZE try: engine_module.BUFFER_SIZE = n yield finally: engine_module.BUFFER_SIZE = original def shrinking_from(start): def accept(f): runner = ConjectureRunner( f, settings=settings( max_examples=5000, database=None, suppress_health_check=list(HealthCheck), # avoid running the explain phase in shrinker.shrink() in tests # which don't test the inquisitor. phases=set(settings.default.phases) - {Phase.explain}, ), random=Random(0), ) runner.cached_test_function(start) assert runner.interesting_examples (last_data,) = runner.interesting_examples.values() return runner.new_shrinker(last_data, lambda d: d.status == Status.INTERESTING) return accept def fresh_data(*, random=None, observer=None) -> ConjectureData: context = current_build_context() if currently_in_test_context() else None if context is not None and settings().backend == "crosshair": # we should reeaxmine fresh_data sometime and see if we can replace it # with nicer and higher level hypothesis idioms. # # For now it doesn't work well with crosshair tests. This is no big # loss, because these tests often rely on hypothesis-provider-specific # things. pytest.skip( "Fresh data is too low level (and too much of a hack) to be " "worth supporting when testing with crosshair" ) if random is None: if context is None: # ensure usage of fresh_data() is not flaky outside of property tests. raise ValueError( "must pass a seeded Random instance to fresh_data() when " "outside of a build context" ) from None # within property tests, ensure fresh_data uses a controlled source of # randomness. # drawing this from the current build context is almost *too* magical. But # the alternative is an extra @given(st.randoms()) everywhere we use # fresh_data, so eh. # @example uses a zero-length data, which means we can't use a # hypothesis-backed random (which would entail drawing from the data). # In this case, use a deterministic Random(0). random = ( context.data.draw(st.randoms(use_true_random=True)) if (choices := context.data.max_choices) is None or choices > 0 else Random(0) ) return ConjectureData(random=random, observer=observer) def clamped_shrink_towards(constraints): v = constraints["shrink_towards"] if constraints["min_value"] is not None: v = max(constraints["min_value"], v) if constraints["max_value"] is not None: v = min(constraints["max_value"], v) return v def draw_value(choice_type, constraints): data = fresh_data() return getattr(data, f"draw_{choice_type}")(**constraints) @st.composite def choices(draw): (choice_type, constraints) = draw(choice_types_constraints()) return draw_value(choice_type, constraints) @st.composite def nodes(draw, *, was_forced=None, choice_types=None): if choice_types is None: (choice_type, constraints) = draw(choice_types_constraints()) else: choice_type = draw(st.sampled_from(choice_types)) constraints = draw(constraints_strategy(choice_type)) # choice nodes don't include forced in their constraints. see was_forced attribute del constraints["forced"] value = draw_value(choice_type, constraints) was_forced = draw(st.booleans()) if was_forced is None else was_forced return ChoiceNode( type=choice_type, value=value, constraints=constraints, was_forced=was_forced ) def nodes_inline(*values: list[ChoiceT]) -> list[ChoiceNode]: """ For inline-creating a choice node or list of choice nodes, where you don't care about the constraints. This uses maximally-permissable constraints and infers the choice_type you meant based on the type of the value. You can optionally pass (value, constraints) to as an element in order to override the default constraints for that element. """ mapping = { float: ( "float", { "min_value": -math.inf, "max_value": math.inf, "allow_nan": True, "smallest_nonzero_magnitude": SMALLEST_SUBNORMAL, }, ), int: ( "integer", { "min_value": None, "max_value": None, "weights": None, "shrink_towards": 0, }, ), str: ( "string", { "intervals": IntervalSet(((0, sys.maxunicode),)), "min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE, }, ), bytes: ("bytes", {"min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE}), bool: ("boolean", {"p": 0.5}), } nodes = [] for value in values: override_constraints = {} if isinstance(value, tuple): (value, override_constraints) = value if override_constraints is None: override_constraints = {} (choice_type, constraints) = mapping[type(value)] nodes.append( ChoiceNode( type=choice_type, value=value, constraints=constraints | override_constraints, was_forced=False, ) ) return tuple(nodes) def float_constr( min_value=-math.inf, max_value=math.inf, *, allow_nan=True, smallest_nonzero_magnitude=SMALLEST_SUBNORMAL, ): return { "min_value": float(min_value), "max_value": float(max_value), "allow_nan": allow_nan, "smallest_nonzero_magnitude": float(smallest_nonzero_magnitude), } def integer_constr(min_value=None, max_value=None, *, weights=None, shrink_towards=0): return { "min_value": min_value, "max_value": max_value, "weights": weights, "shrink_towards": shrink_towards, } def string_constr(intervals, *, min_size=0, max_size=COLLECTION_DEFAULT_MAX_SIZE): return {"intervals": intervals, "min_size": min_size, "max_size": max_size} # we could in theory define bytes_constr and boolean_constr, but without any # default kw values they aren't really a time save. ================================================ FILE: hypothesis-python/tests/conjecture/test_choice.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys from copy import deepcopy import pytest from hypothesis import ( HealthCheck, assume, example, given, note, settings, strategies as st, ) from hypothesis.errors import StopTest from hypothesis.internal.conjecture.choice import ( ChoiceNode, ChoiceTemplate, choice_equal, choice_from_index, choice_permitted, choice_to_index, choices_key, ) from hypothesis.internal.conjecture.data import ( COLLECTION_DEFAULT_MAX_SIZE, ConjectureData, Status, choices_size, ) from hypothesis.internal.conjecture.datatree import ( MAX_CHILDREN_EFFECTIVELY_INFINITE, all_children, compute_max_children, ) from hypothesis.internal.conjecture.engine import choice_count from hypothesis.internal.conjecture.provider_conformance import integer_constraints from hypothesis.internal.floats import SMALLEST_SUBNORMAL, next_down, next_up from hypothesis.internal.intervalsets import IntervalSet from tests.common.debug import minimal from tests.conjecture.common import ( choice_types_constraints, clamped_shrink_towards, draw_value, float_constr, fresh_data, integer_constr, nodes, ) # we max out at 128 bit integers in the *unbounded* case, but someone may # specify a bound with a larger magnitude. Ensure we calculate max children for # those cases correctly. @example(("integer", integer_constr(max_value=-(2**200)))) @example(("integer", integer_constr(min_value=2**200))) @example(("integer", integer_constr(-(2**200), 2**200))) @given(choice_types_constraints()) def test_compute_max_children_is_positive(choice_type_and_constraints): choice_type, constraints = choice_type_and_constraints assert compute_max_children(choice_type, constraints) >= 0 @pytest.mark.parametrize( "choice_type, constraints, count_children", [ ("integer", {"min_value": 1, "max_value": 2, "weights": {1: 0.1, 2: 0.1}}, 2), # only possibility is the empty string ( "string", {"min_size": 0, "max_size": 100, "intervals": IntervalSet.from_string("")}, 1, ), ( "string", {"min_size": 0, "max_size": 0, "intervals": IntervalSet.from_string("abc")}, 1, ), # 3 possibilities for each character, 8 characters, 3 ** 8 possibilities. ( "string", {"min_size": 8, "max_size": 8, "intervals": IntervalSet.from_string("abc")}, 3**8, ), ( "string", { "min_size": 2, "max_size": 8, "intervals": IntervalSet.from_string("abcd"), }, sum(4**k for k in range(2, 8 + 1)), ), ( "string", { "min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE, "intervals": IntervalSet.from_string("a"), }, COLLECTION_DEFAULT_MAX_SIZE + 1, ), ( "string", { "min_size": 0, "max_size": 10_000, "intervals": IntervalSet.from_string("abcdefg"), }, MAX_CHILDREN_EFFECTIVELY_INFINITE, ), ( "bytes", { "min_size": 0, "max_size": 2, }, sum(2 ** (8 * k) for k in range(2 + 1)), ), ( "bytes", { "min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE, }, MAX_CHILDREN_EFFECTIVELY_INFINITE, ), ( "bytes", { "min_size": 0, "max_size": 10_000, }, MAX_CHILDREN_EFFECTIVELY_INFINITE, ), ("boolean", {"p": 0.0}, 1), ("boolean", {"p": 1.0}, 1), ("boolean", {"p": 0.5}, 2), ("boolean", {"p": 0.001}, 2), ("boolean", {"p": 0.999}, 2), ("float", float_constr(0.0, 0.0), 1), ("float", float_constr(-0.0, -0.0), 1), ("float", float_constr(-0.0, 0.0), 2), ("float", float_constr(next_down(-0.0), next_up(0.0)), 4), ( "float", float_constr( next_down(next_down(-0.0)), next_up(next_up(0.0)), smallest_nonzero_magnitude=next_up(SMALLEST_SUBNORMAL), ), 4, ), ("float", float_constr(smallest_nonzero_magnitude=next_down(math.inf)), 6), ("float", float_constr(1, 10, smallest_nonzero_magnitude=11.0), 0), ("float", float_constr(-3, -2, smallest_nonzero_magnitude=4.0), 0), ], ) def test_compute_max_children(choice_type, constraints, count_children): assert compute_max_children(choice_type, constraints) == count_children @given(st.text(min_size=1, max_size=1), st.integers(0, 100)) def test_draw_string_single_interval_with_equal_bounds(s, n): data = fresh_data() intervals = IntervalSet.from_string(s) assert data.draw_string(intervals, min_size=n, max_size=n) == s * n @example(("boolean", {"p": 2**-65})) @example(("boolean", {"p": 1 - 2**-65})) @example( ( "string", {"min_size": 0, "max_size": 0, "intervals": IntervalSet.from_string("abc")}, ) ) @example( ("string", {"min_size": 0, "max_size": 3, "intervals": IntervalSet.from_string("")}) ) @example( ( "string", {"min_size": 0, "max_size": 3, "intervals": IntervalSet.from_string("a")}, ) ) # all combinations of float signs @example(("float", float_constr(next_down(-0.0), -0.0))) @example(("float", float_constr(next_down(-0.0), next_up(0.0)))) @example(("float", float_constr(0.0, next_up(0.0)))) # using a smallest_nonzero_magnitude which happens to filter out everything @example(("float", float_constr(1.0, 2.0, smallest_nonzero_magnitude=3.0))) @example(("integer", integer_constr(1, 2, weights={1: 0.2, 2: 0.4}))) @given(choice_types_constraints()) @settings(suppress_health_check=[HealthCheck.filter_too_much]) def test_compute_max_children_and_all_children_agree(choice_type_and_constraints): choice_type, constraints = choice_type_and_constraints max_children = compute_max_children(choice_type, constraints) # avoid slowdowns / OOM when reifying extremely large all_children generators. # We also hard cap at MAX_CHILDREN_EFFECTIVELY_INFINITE, because max_children # returns approximations after this value and so will disagree with # all_children. cap = min(100_000, MAX_CHILDREN_EFFECTIVELY_INFINITE) assume(max_children < cap) assert len(list(all_children(choice_type, constraints))) == max_children # it's very hard to test that unbounded integer ranges agree with # compute_max_children, because they by necessity require iterating over 2**127 # or more elements. We do the not great approximation of checking just the first # element is what we expect. @given(integer_constraints()) def test_compute_max_children_unbounded_integer_ranges(constraints): expected = clamped_shrink_towards(constraints) first = next(all_children("integer", constraints)) assert expected == first, (expected, first) @given(st.randoms()) def test_nodes(random): data = fresh_data(random=random) data.draw_float(min_value=-10.0, max_value=10.0, forced=5.0) data.draw_boolean(forced=True) data.start_span(42) data.draw_string(IntervalSet.from_string("abcd"), forced="abbcccdddd") data.draw_bytes(8, 8, forced=bytes(8)) data.stop_span() data.draw_integer(0, 100, forced=50) data.freeze() expected_tree_nodes = ( ChoiceNode( type="float", value=5.0, constraints=float_constr(-10.0, 10.0), was_forced=True, ), ChoiceNode( type="boolean", value=True, constraints={"p": 0.5}, was_forced=True, ), ChoiceNode( type="string", value="abbcccdddd", constraints={ "intervals": IntervalSet.from_string("abcd"), "min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE, }, was_forced=True, ), ChoiceNode( type="bytes", value=bytes(8), constraints={"min_size": 8, "max_size": 8}, was_forced=True, ), ChoiceNode( type="integer", value=50, constraints=integer_constr(0, 100), was_forced=True, ), ) assert data.nodes == expected_tree_nodes @given(nodes()) def test_copy_choice_node(node): assert node == node assume(not node.was_forced) new_value = draw_value(node.type, node.constraints) # if we drew the same value as before, the node should still be equal assert (node.copy(with_value=new_value) == node) is ( choice_equal(new_value, node.value) ) @given(nodes()) def test_choice_node_equality(node): assert node == node # for coverage on our NotImplemented return, more than anything. assert node != 42 @given(nodes(was_forced=True)) def test_cannot_modify_forced_nodes(node): with pytest.raises(AssertionError): node.copy(with_value=42) def test_data_with_empty_choices_is_overrun(): data = ConjectureData.for_choices([]) with pytest.raises(StopTest): data.draw_integer() assert data.status is Status.OVERRUN @given(nodes(was_forced=True)) def test_data_with_changed_forced_value(node): # we had a forced node and then tried to draw a different forced value from it. # nodes: v1 [was_forced=True] # drawing: [forced=v2] # # This is actually fine; we'll just ignore the forced node (v1) and return # what the draw expects (v2). data = ConjectureData.for_choices([node.value]) draw_func = getattr(data, f"draw_{node.type}") constraints = deepcopy(node.constraints) constraints["forced"] = draw_value(node.type, node.constraints) assume(not choice_equal(constraints["forced"], node.value)) assert choice_equal(draw_func(**constraints), constraints["forced"]) # ensure we hit bare-minimum coverage for all choice sequence types. @example( ChoiceNode(type="float", value=0.0, constraints=float_constr(), was_forced=True) ) @example( ChoiceNode( type="boolean", value=False, constraints={"p": 0.5}, was_forced=True, ) ) @example( ChoiceNode( type="integer", value=50, constraints=integer_constr(50, 100), was_forced=True ) ) @example( ChoiceNode( type="string", value="aaaa", constraints={ "intervals": IntervalSet.from_string("bcda"), "min_size": 4, "max_size": COLLECTION_DEFAULT_MAX_SIZE, }, was_forced=True, ) ) @example( ChoiceNode( type="bytes", value=bytes(8), constraints={"min_size": 8, "max_size": 8}, was_forced=True, ) ) @given(nodes(was_forced=True)) def test_data_with_same_forced_value_is_valid(node): # we had a forced node and then drew the same forced value. This is totally # fine! # nodes: v1 [was_forced=True] # drawing: [forced=v1] data = ConjectureData.for_choices([node.value]) draw_func = getattr(data, f"draw_{node.type}") constraints = deepcopy(node.constraints) constraints["forced"] = node.value assert choice_equal(draw_func(**constraints), constraints["forced"]) @given(choice_types_constraints()) @settings(suppress_health_check=[HealthCheck.filter_too_much]) def test_all_children_are_permitted_values(choice_type_and_constraints): choice_type, constraints = choice_type_and_constraints max_children = compute_max_children(choice_type, constraints) cap = min(100_000, MAX_CHILDREN_EFFECTIVELY_INFINITE) assume(max_children < cap) # test that all_children -> choice_permitted (but not necessarily the converse.) for value in all_children(choice_type, constraints): assert choice_permitted(value, constraints), value @pytest.mark.parametrize( "value, constraints, permitted", [ (0, integer_constr(1, 2), False), (2, integer_constr(0, 1), False), (10, integer_constr(0, 20), True), (int(2**128 / 2) - 1, integer_constr(), True), (int(2**128 / 2), integer_constr(), True), (math.nan, float_constr(0.0, 0.0), True), (math.nan, float_constr(0.0, 0.0, allow_nan=False), False), (2.0, float_constr(1.0, 3.0, smallest_nonzero_magnitude=2.5), False), ( -2.0, float_constr(-3.0, -1.0, smallest_nonzero_magnitude=2.5), False, ), (1.0, float_constr(1.0, 1.0), True), ( "abcd", { "min_size": 10, "max_size": 20, "intervals": IntervalSet.from_string("abcd"), }, False, ), ( "abcd", { "min_size": 1, "max_size": 3, "intervals": IntervalSet.from_string("abcd"), }, False, ), ( "abcd", {"min_size": 1, "max_size": 10, "intervals": IntervalSet.from_string("e")}, False, ), ( "e", {"min_size": 1, "max_size": 10, "intervals": IntervalSet.from_string("e")}, True, ), (b"a", {"min_size": 2, "max_size": 2}, False), (b"aa", {"min_size": 2, "max_size": 2}, True), (b"aa", {"min_size": 0, "max_size": 3}, True), (b"a", {"min_size": 2, "max_size": 10}, False), (True, {"p": 0}, False), (False, {"p": 0}, True), (True, {"p": 1}, True), (False, {"p": 1}, False), (True, {"p": 0.5}, True), (False, {"p": 0.5}, True), ], ) def test_choice_permitted(value, constraints, permitted): assert choice_permitted(value, constraints) == permitted @given(nodes(was_forced=True)) def test_forced_nodes_are_trivial(node): assert node.trivial @pytest.mark.parametrize( "node", [ ChoiceNode( type="float", value=5.0, constraints=float_constr(5.0, 10.0), was_forced=False, ), ChoiceNode( type="float", value=0.0, constraints=float_constr(-5.0, 5.0), was_forced=False, ), ChoiceNode( type="float", value=0.0, constraints=float_constr(), was_forced=False ), ChoiceNode( type="boolean", value=False, constraints={"p": 0.5}, was_forced=False ), ChoiceNode( type="boolean", value=True, constraints={"p": 1.0}, was_forced=False ), ChoiceNode( type="boolean", value=False, constraints={"p": 0.0}, was_forced=False ), ChoiceNode( type="string", value="", constraints={ "intervals": IntervalSet.from_string("abcd"), "min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE, }, was_forced=False, ), ChoiceNode( type="string", value="aaaa", constraints={ "intervals": IntervalSet.from_string("bcda"), "min_size": 4, "max_size": COLLECTION_DEFAULT_MAX_SIZE, }, was_forced=False, ), ChoiceNode( type="bytes", value=bytes(8), constraints={"min_size": 8, "max_size": 8}, was_forced=False, ), ChoiceNode( type="bytes", value=bytes(2), constraints={"min_size": 2, "max_size": COLLECTION_DEFAULT_MAX_SIZE}, was_forced=False, ), ChoiceNode( type="integer", value=50, constraints=integer_constr(50, 100), was_forced=False, ), ChoiceNode( type="integer", value=0, constraints=integer_constr(-10, 10), was_forced=False, ), ChoiceNode( type="integer", value=2, constraints=integer_constr(-10, 10, shrink_towards=2), was_forced=False, ), ChoiceNode( type="integer", value=-10, constraints=integer_constr(-10, 10, shrink_towards=-12), was_forced=False, ), ChoiceNode( type="integer", value=10, constraints=integer_constr(-10, 10, shrink_towards=12), was_forced=False, ), ChoiceNode( type="integer", value=0, constraints=integer_constr(), was_forced=False ), ChoiceNode( type="integer", value=1, constraints=integer_constr(min_value=-10, shrink_towards=1), was_forced=False, ), ChoiceNode( type="integer", value=1, constraints=integer_constr(max_value=10, shrink_towards=1), was_forced=False, ), ChoiceNode( type="integer", value=1, constraints={ "min_value": None, "max_value": None, "weights": None, "shrink_towards": 1, }, was_forced=False, ), ], ) def test_trivial_nodes(node): assert node.trivial @st.composite def values(draw): data = draw(st.data()).conjecture_data return getattr(data, f"draw_{node.type}")(**node.constraints) # if we're trivial, then shrinking should produce the same value. assert choice_equal(minimal(values()), node.value) @pytest.mark.parametrize( "node", [ ChoiceNode( type="float", value=6.0, constraints=float_constr(5.0, 10.0), was_forced=False, ), ChoiceNode( type="float", value=-5.0, constraints=float_constr(-5.0, 5.0), was_forced=False, ), ChoiceNode( type="float", value=1.0, constraints=float_constr(), was_forced=False ), ChoiceNode( type="boolean", value=True, constraints={"p": 0.5}, was_forced=False ), ChoiceNode( type="boolean", value=True, constraints={"p": 0.99}, was_forced=False ), ChoiceNode( type="string", value="d", constraints={ "intervals": IntervalSet.from_string("abcd"), "min_size": 1, "max_size": COLLECTION_DEFAULT_MAX_SIZE, }, was_forced=False, ), ChoiceNode( type="bytes", value=b"\x01", constraints={"min_size": 1, "max_size": 1}, was_forced=False, ), ChoiceNode( type="bytes", value=bytes(1), constraints={"min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE}, was_forced=False, ), ChoiceNode( type="bytes", value=bytes(2), constraints={"min_size": 1, "max_size": 10}, was_forced=False, ), ChoiceNode( type="integer", value=-10, constraints=integer_constr(-10, 10), was_forced=False, ), ChoiceNode( type="integer", value=42, constraints=integer_constr(), was_forced=False ), ], ) def test_nontrivial_nodes(node): assert not node.trivial @st.composite def values(draw): data = draw(st.data()).conjecture_data return getattr(data, f"draw_{node.type}")(**node.constraints) # if we're nontrivial, then shrinking should produce something different. assert not choice_equal(minimal(values()), node.value) @pytest.mark.parametrize( "node", [ ChoiceNode( type="float", value=1.5, constraints=float_constr(1.1, 1.6), was_forced=False, ), ChoiceNode( type="float", value=float(math.floor(sys.float_info.max)), constraints=float_constr(sys.float_info.max - 1, math.inf), was_forced=False, ), ChoiceNode( type="float", value=float(math.ceil(-sys.float_info.max)), constraints=float_constr(-math.inf, -sys.float_info.max + 1), was_forced=False, ), ChoiceNode( type="float", value=math.inf, constraints=float_constr(math.inf, math.inf), was_forced=False, ), ChoiceNode( type="float", value=-math.inf, constraints=float_constr(-math.inf, -math.inf), was_forced=False, ), ], ) def test_conservative_nontrivial_nodes(node): # these nodes actually are trivial, but our analysis doesn't compute them # as such. We'd like to improve this in the future! assert not node.trivial @st.composite def values(draw): data = draw(st.data()).conjecture_data return getattr(data, f"draw_{node.type}")(**node.constraints) assert choice_equal(minimal(values()), node.value) @given(nodes()) def test_choice_node_is_hashable(node): hash(node) @given(st.lists(nodes())) def test_choices_size_positive(nodes): assert choices_size([n.value for n in nodes]) >= 0 @given(st.integers(min_value=1)) def test_node_template_count(n): node = ChoiceTemplate(type="simplest", count=n) assert choice_count([node]) == n def test_node_template_to_overrun(): data = ConjectureData.for_choices([1, ChoiceTemplate("simplest", count=5)]) data.draw_integer() with pytest.raises(StopTest): for _ in range(10): data.draw_integer() assert data.status is Status.OVERRUN def test_node_template_single_node_overruns(): # test for when drawing a single node takes more than BUFFER_SIZE, while in # the ChoiceTemplate case data = ConjectureData.for_choices((ChoiceTemplate("simplest", count=1),)) with pytest.raises(StopTest): data.draw_bytes(10_000, 10_000) assert data.status is Status.OVERRUN @given(nodes()) def test_node_template_simplest_is_actually_trivial(node): # TODO_IR node.trivial is sound but not complete for floats. assume(node.type != "float") data = ConjectureData.for_choices((ChoiceTemplate("simplest", count=1),)) getattr(data, f"draw_{node.type}")(**node.constraints) assert len(data.nodes) == 1 assert data.nodes[0].trivial @given(choice_types_constraints()) @example(("boolean", {"p": 0})) @example(("boolean", {"p": 1})) def test_choice_indices_are_positive(choice_type_and_constraints): choice_type, constraints = choice_type_and_constraints v = draw_value(choice_type, constraints) assert choice_to_index(v, constraints) >= 0 @given(integer_constraints()) def test_shrink_towards_has_index_0(constraints): shrink_towards = clamped_shrink_towards(constraints) note({"clamped_shrink_towards": shrink_towards}) assert choice_to_index(shrink_towards, constraints) == 0 assert choice_from_index(0, "integer", constraints) == shrink_towards @given(choice_types_constraints()) @settings(max_examples=20) def test_choice_to_index_injective(choice_type_and_constraints): # choice sequence ordering should be injective both ways. choice_type, constraints = choice_type_and_constraints # ...except for floats, which are hard to order bijectively. assume(choice_type != "float") # cap to 10k so this test finishes in a reasonable amount of time cap = min(compute_max_children(choice_type, constraints), 10_000) indices = set() for i, choice in enumerate(all_children(choice_type, constraints)): if i >= cap: break index = choice_to_index(choice, constraints) assert index not in indices indices.add(index) @given(choice_types_constraints()) @settings(max_examples=10) @example( ( "string", {"min_size": 0, "max_size": 10, "intervals": IntervalSet.from_string("a")}, ) ) def test_choice_from_value_injective(choice_type_and_constraints): choice_type, constraints = choice_type_and_constraints assume(choice_type != "float") cap = min(compute_max_children(choice_type, constraints), 10_000) choices = set() for index in range(cap): choice = choice_from_index(index, choice_type, constraints) assert choice not in choices choices.add(choice) @given(choice_types_constraints()) def test_choice_index_and_value_are_inverses(choice_type_and_constraints): choice_type, constraints = choice_type_and_constraints v = draw_value(choice_type, constraints) index = choice_to_index(v, constraints) note({"v": v, "index": index}) choice_equal(choice_from_index(index, choice_type, constraints), v) @pytest.mark.parametrize( "choice_type, constraints, choices", [ ("boolean", {"p": 1}, [True]), ("boolean", {"p": 0}, [False]), ("integer", integer_constr(min_value=1, shrink_towards=4), range(1, 10)), ("integer", integer_constr(max_value=5, shrink_towards=2), range(-10, 5 + 1)), ("integer", integer_constr(max_value=5), range(-10, 5 + 1)), ("integer", integer_constr(min_value=0, shrink_towards=1), range(10)), ("integer", integer_constr(-5, 5, shrink_towards=3), range(-5, 5 + 1)), ("integer", integer_constr(-5, 5, shrink_towards=-3), range(-5, 5 + 1)), ( "float", float_constr(1.0, next_up(next_up(1.0))), [1.0, next_up(1.0), next_up(next_up(1.0))], ), ( "float", float_constr(next_down(-0.0), next_up(0.0)), [next_down(-0.0), -0.0, 0.0, next_up(0.0)], ), ], ) def test_choice_index_and_value_are_inverses_explicit( choice_type, constraints, choices ): for choice in choices: index = choice_to_index(choice, constraints) assert choice_equal(choice_from_index(index, choice_type, constraints), choice) @pytest.mark.parametrize( "constraints, choices", [ # unbounded (integer_constr(), (0, 1, -1, 2, -2, 3, -3)), (integer_constr(shrink_towards=2), (2, 3, 1, 4, 0, 5, -1, 6, -2)), # semibounded (below) (integer_constr(min_value=3), (3, 4, 5, 6, 7)), (integer_constr(min_value=3, shrink_towards=5), (5, 6, 4, 7, 3, 8, 9)), (integer_constr(min_value=-3), (0, 1, -1, 2, -2, 3, -3, 4, 5, 6)), (integer_constr(min_value=-3, shrink_towards=-1), (-1, 0, -2, 1, -3, 2, 3, 4)), # semibounded (above) (integer_constr(max_value=3), (0, 1, -1, 2, -2, 3, -3, -4, -5, -6)), (integer_constr(max_value=3, shrink_towards=1), (1, 2, 0, 3, -1, -2, -3, -4)), (integer_constr(max_value=-3), (-3, -4, -5, -6, -7)), (integer_constr(max_value=-3, shrink_towards=-5), (-5, -4, -6, -3, -7, -8, -9)), # bounded (integer_constr(-3, 3), (0, 1, -1, 2, -2, 3, -3)), (integer_constr(-3, 3, shrink_towards=1), (1, 2, 0, 3, -1, -2, -3)), (integer_constr(-3, 3, shrink_towards=-1), (-1, 0, -2, 1, -3, 2, 3)), ], ids=repr, ) def test_integer_choice_index(constraints, choices): # explicit test which checks that the order of `choices` matches the index # order. for i, choice in enumerate(choices): assert choice_to_index(choice, constraints) == i @given(st.lists(nodes())) def test_drawing_directly_matches_for_choices(nodes): data = ConjectureData.for_choices([n.value for n in nodes]) for node in nodes: value = getattr(data, f"draw_{node.type}")(**node.constraints) assert choice_equal(node.value, value) def test_draw_directly_explicit(): # this is a much weaker and more explicit variant of the property-based test # directly above, but this is such an important thing to ensure that we have # correct that it's worth some duplication in case we ever screw up our pbt test. assert ( ConjectureData.for_choices(["a"]).draw_string( IntervalSet([(0, 127)]), min_size=1 ) == "a" ) assert ConjectureData.for_choices([b"a"]).draw_bytes() == b"a" assert ( ConjectureData.for_choices([1.0]).draw_float( 0.0, 2.0, allow_nan=False, smallest_nonzero_magnitude=0.5 ) == 1.0 ) assert ConjectureData.for_choices([True]).draw_boolean(0.3) assert ConjectureData.for_choices([42]).draw_integer() == 42 assert ( ConjectureData.for_choices([-42]).draw_integer(min_value=-50, max_value=0) == -42 ) assert ( ConjectureData.for_choices([10]).draw_integer( min_value=10, max_value=11, weights={10: 0.1, 11: 0.3} ) == 10 ) @pytest.mark.parametrize( "choices1, choices2", [ [(True,), (1,)], [(True,), (1.0,)], [(False,), (0,)], [(False,), (0.0,)], [(False,), (-0.0,)], [(0.0,), (-0.0,)], ], ) def test_choices_key_distinguishes_weird_cases(choices1, choices2): assert choices_key(choices1) != choices_key(choices2) def test_node_template_overrun(): # different code path for overruning the ChoiceTemplate count, not BUFFER_SIZE. cd = ConjectureData( random=None, prefix=[ChoiceTemplate("simplest", count=2)], max_choices=100, ) cd.draw_integer() cd.draw_integer() try: cd.draw_integer() except StopTest: pass assert cd.status is Status.OVERRUN ================================================ FILE: hypothesis-python/tests/conjecture/test_choice_tree.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from random import Random from hypothesis import given, strategies as st from hypothesis.internal.conjecture.shrinking.choicetree import ( ChoiceTree, prefix_selection_order, random_selection_order, ) def select(*args): return prefix_selection_order(args) def exhaust(f): tree = ChoiceTree() results = [] prefix = () while not tree.exhausted: prefix = tree.step( prefix_selection_order(prefix), lambda chooser: results.append(f(chooser)) ) return results @given(st.lists(st.integers())) def test_can_enumerate_a_shallow_set(ls): results = exhaust(lambda chooser: chooser.choose(ls)) assert sorted(results) == sorted(ls) def test_can_enumerate_a_nested_set(): @exhaust def nested(chooser): i = chooser.choose(range(10)) j = chooser.choose(range(10), condition=lambda j: j > i) return (i, j) assert sorted(nested) == [(i, j) for i in range(10) for j in range(i + 1, 10)] def test_can_enumerate_empty(): @exhaust def empty(chooser): return 1 assert empty == [1] def test_all_filtered_child(): @exhaust def all_filtered(chooser): chooser.choose(range(10), condition=lambda j: False) assert all_filtered == [] def test_skips_over_exhausted_children(): results = [] def f(chooser): results.append( ( chooser.choose(range(3), condition=lambda x: x > 0), chooser.choose(range(2)), ) ) tree = ChoiceTree() tree.step(select(1, 0), f) tree.step(select(1, 1), f) tree.step(select(0, 0), f) assert results == [(1, 0), (1, 1), (2, 0)] def test_extends_prefix_from_right(): def f(chooser): chooser.choose(range(4)) tree = ChoiceTree() result = tree.step(select(), f) assert result == (3,) def test_starts_from_the_end(): def f(chooser): chooser.choose(range(3)) tree = ChoiceTree() assert tree.step(select(), f) == (2,) def test_skips_over_exhausted_subtree(): def f(chooser): chooser.choose(range(10)) tree = ChoiceTree() assert tree.step(select(8), f) == (8,) assert tree.step(select(8), f) == (7,) def test_exhausts_randomly(): def f(chooser): chooser.choose(range(10)) tree = ChoiceTree() random = Random() seen = set() for _ in range(10): seen.add(tree.step(random_selection_order(random), f)) assert len(seen) == 10 assert tree.exhausted def test_exhausts_randomly_when_filtering(): def f(chooser): chooser.choose(range(10), lambda x: False) tree = ChoiceTree() random = Random() tree.step(random_selection_order(random), f) assert tree.exhausted ================================================ FILE: hypothesis-python/tests/conjecture/test_data_tree.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import textwrap from random import Random import pytest from hypothesis import HealthCheck, assume, given, settings from hypothesis.errors import Flaky from hypothesis.internal.conjecture.choice import ChoiceTemplate from hypothesis.internal.conjecture.data import ConjectureData, Status, StopTest from hypothesis.internal.conjecture.datatree import ( Branch, DataTree, compute_max_children, ) from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.conjecture.floats import float_to_int from hypothesis.internal.conjecture.provider_conformance import ( boolean_constraints, integer_constraints, ) from hypothesis.internal.floats import next_up from hypothesis.vendor import pretty from tests.conjecture.common import ( constraints_strategy, fresh_data, interesting_origin, nodes, run_to_nodes, ) runner_settings = settings( max_examples=100, database=None, suppress_health_check=list(HealthCheck) ) def runner_for(*examples): def accept(tf): runner = ConjectureRunner( tf, settings=runner_settings, random=Random(0), ignore_limits=True ) ran_examples = [] for choices in examples: data = runner.cached_test_function(choices) ran_examples.append((choices, data)) for choices, d in ran_examples: rewritten, status = runner.tree.rewrite(choices) assert status == d.status assert rewritten == d.choices return runner return accept def test_can_lookup_cached_examples(): @runner_for((0, 0), (0, 1)) def runner(data): data.draw_integer() data.draw_integer() def test_can_lookup_cached_examples_with_forced(): @runner_for((0, 0), (0, 1)) def runner(data): data.draw_integer(forced=1) data.draw_integer() def test_can_detect_when_tree_is_exhausted(): @runner_for((False,), (True,)) def runner(data): data.draw_boolean() assert runner.tree.is_exhausted def test_can_detect_when_tree_is_exhausted_variable_size(): @runner_for((False,), (True, False), (True, True)) def runner(data): if data.draw_boolean(): data.draw_boolean() assert runner.tree.is_exhausted def test_one_dead_branch(): @runner_for(*([(0, i) for i in range(16)] + [(i,) for i in range(1, 16)])) def runner(data): i = data.draw_integer(0, 15) if i > 0: data.mark_invalid() data.draw_integer(0, 15) assert runner.tree.is_exhausted def test_non_dead_root(): @runner_for((False, False), (True, False), (True, True)) def runner(data): data.draw_boolean() data.draw_boolean() def test_can_reexecute_dead_examples(): @runner_for((False, False), (False, True), (False, False)) def runner(data): data.draw_boolean() data.draw_boolean() def test_novel_prefixes_are_novel(): def tf(data): for _ in range(4): data.draw_bytes(1, 1, forced=b"\0") data.draw_integer(0, 3) runner = ConjectureRunner( tf, settings=settings(runner_settings, max_examples=1000), random=Random(0) ) for _ in range(100): prefix = runner.tree.generate_novel_prefix(runner.random) extension = prefix + (ChoiceTemplate("simplest", count=100),) assert runner.tree.rewrite(extension)[1] is None result = runner.cached_test_function(extension) assert runner.tree.rewrite(extension)[0] == result.choices def test_overruns_if_prefix(): runner = ConjectureRunner( lambda data: [data.draw_boolean() for _ in range(2)], settings=runner_settings, random=Random(0), ) runner.cached_test_function([False, False]) assert runner.tree.rewrite([False])[1] is Status.OVERRUN def test_stores_the_tree_flat_until_needed(): @runner_for((False,) * 10) def runner(data): for _ in range(10): data.draw_boolean() data.mark_interesting(interesting_origin()) root = runner.tree.root assert len(root.constraints) == 10 assert len(root.values) == 10 assert root.transition.status == Status.INTERESTING def test_split_in_the_middle(): @runner_for((0, 0, 2), (0, 1, 3)) def runner(data): data.draw_integer(0, 1) data.draw_integer(0, 1) data.draw_integer(0, 15) data.mark_interesting(interesting_origin()) root = runner.tree.root assert len(root.constraints) == len(root.values) == 1 assert list(root.transition.children[0].values) == [2] assert list(root.transition.children[1].values) == [3] def test_stores_forced_nodes(): @runner_for((0, 0, 0)) def runner(data): data.draw_integer(0, 1, forced=0) data.draw_integer(0, 1) data.draw_integer(0, 1, forced=0) data.mark_interesting(interesting_origin()) root = runner.tree.root assert root.forced == {0, 2} def test_correctly_relocates_forced_nodes(): @runner_for((0, 0), (1, 0)) def runner(data): data.draw_integer(0, 1) data.draw_integer(0, 1, forced=0) data.mark_interesting(interesting_origin()) root = runner.tree.root assert root.transition.children[1].forced == {0} assert root.transition.children[0].forced == {0} def test_can_go_from_interesting_to_valid(): tree = DataTree() data = ConjectureData.for_choices([], observer=tree.new_observer()) with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices([], observer=tree.new_observer()) with pytest.raises(StopTest): data.conclude_test(Status.VALID) def test_going_from_interesting_to_invalid_is_flaky(): tree = DataTree() data = ConjectureData.for_choices([], observer=tree.new_observer()) with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices([], observer=tree.new_observer()) with pytest.raises(Flaky): data.conclude_test(Status.INVALID) def test_concluding_at_prefix_is_flaky(): tree = DataTree() data = ConjectureData.for_choices((True,), observer=tree.new_observer()) data.draw_boolean() with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices([], observer=tree.new_observer()) with pytest.raises(Flaky): data.conclude_test(Status.INVALID) def test_concluding_with_overrun_at_prefix_is_not_flaky(): tree = DataTree() data = ConjectureData.for_choices((True,), observer=tree.new_observer()) data.draw_boolean() with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices([], observer=tree.new_observer()) with pytest.raises(StopTest): data.conclude_test(Status.OVERRUN) def test_changing_n_bits_is_flaky_in_prefix(): tree = DataTree() data = ConjectureData.for_choices((1,), observer=tree.new_observer()) data.draw_integer(0, 1) with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices((1,), observer=tree.new_observer()) with pytest.raises(Flaky): data.draw_integer(0, 3) def test_changing_n_bits_is_flaky_in_branch(): tree = DataTree() for i in [0, 1]: data = ConjectureData.for_choices((i,), observer=tree.new_observer()) data.draw_integer(0, 1) with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices((1,), observer=tree.new_observer()) with pytest.raises(Flaky): data.draw_integer(0, 3) def test_extending_past_conclusion_is_flaky(): tree = DataTree() data = ConjectureData.for_choices((True,), observer=tree.new_observer()) data.draw_boolean() with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices((True, False), observer=tree.new_observer()) data.draw_boolean() with pytest.raises(Flaky): data.draw_boolean() def test_changing_to_forced_is_flaky(): tree = DataTree() data = ConjectureData.for_choices((True,), observer=tree.new_observer()) data.draw_boolean() with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices((True, False), observer=tree.new_observer()) with pytest.raises(Flaky): data.draw_boolean(forced=True) def test_changing_value_of_forced_is_flaky(): tree = DataTree() data = ConjectureData.for_choices((True,), observer=tree.new_observer()) data.draw_boolean(forced=True) with pytest.raises(StopTest): data.conclude_test(Status.INTERESTING) data = ConjectureData.for_choices((True, False), observer=tree.new_observer()) with pytest.raises(Flaky): data.draw_boolean(forced=False) def test_does_not_truncate_if_unseen(): tree = DataTree() nodes = (1, 2, 3, 4) assert tree.rewrite(nodes) == (nodes, None) def test_truncates_if_seen(): tree = DataTree() nodes = (1, 2, 3, 4) data = ConjectureData.for_choices(nodes, observer=tree.new_observer()) data.draw_integer() data.draw_integer() data.freeze() assert tree.rewrite(nodes) == (nodes[:2], Status.VALID) def test_child_becomes_exhausted_after_split(): tree = DataTree() data = ConjectureData.for_choices((b"\0", b"\0"), observer=tree.new_observer()) data.draw_bytes(1, 1) data.draw_bytes(1, 1, forced=b"\0") data.freeze() data = ConjectureData.for_choices((b"\1", b"\0"), observer=tree.new_observer()) data.draw_bytes(1, 1) data.draw_bytes(1, 1) data.freeze() assert not tree.is_exhausted assert tree.root.transition.children[b"\0"].is_exhausted def test_will_generate_novel_prefix_to_avoid_exhausted_branches(): tree = DataTree() data = ConjectureData.for_choices((1,), observer=tree.new_observer()) data.draw_integer(0, 1) data.freeze() data = ConjectureData.for_choices((0, b"\1"), observer=tree.new_observer()) data.draw_integer(0, 1) data.draw_bytes(1, 1) data.freeze() prefix = tree.generate_novel_prefix(Random(0)) assert len(prefix) == 2 assert prefix[0] == 0 def test_will_mark_changes_in_discard_as_flaky(): tree = DataTree() data = ConjectureData.for_choices((1, 1), observer=tree.new_observer()) data.start_span(10) data.draw_integer(0, 1) data.stop_span() data.draw_integer(0, 1) data.freeze() data = ConjectureData.for_choices((1, 1), observer=tree.new_observer()) data.start_span(10) data.draw_integer(0, 1) with pytest.raises(Flaky): data.stop_span(discard=True) def test_is_not_flaky_on_positive_zero_and_negative_zero(): # if we store floats in a naive way, the 0.0 and -0.0 draws will be treated # equivalently and will lead to flaky errors when they diverge on the boolean # draw. tree = DataTree() data = ConjectureData.for_choices((0.0, False), observer=tree.new_observer()) f = data.draw_float() assert float_to_int(f) == float_to_int(0.0) data.draw_boolean() data.freeze() data = ConjectureData.for_choices((-0.0, True), observer=tree.new_observer()) f = data.draw_float() assert float_to_int(f) == float_to_int(-0.0) data.draw_boolean() data.freeze() assert isinstance(tree.root.transition, Branch) children = tree.root.transition.children assert children[float_to_int(0.0)].values == [False] assert children[float_to_int(-0.0)].values == [True] def test_low_probabilities_are_still_explored(): tree = DataTree() data = ConjectureData.for_choices([False], observer=tree.new_observer()) data.draw_boolean(p=1e-10) # False prefix = tree.generate_novel_prefix(Random()) assert prefix[0] def _test_observed_draws_are_recorded_in_tree(choice_type): @given(constraints_strategy(choice_type)) def test(constraints): # we currently split pseudo-choices with a single child into their # own transition, which clashes with our asserts below. If we ever # change this (say, by not writing pseudo choices to the ir at all), # this restriction can be relaxed. assume(compute_max_children(choice_type, constraints) > 1) tree = DataTree() data = fresh_data(observer=tree.new_observer()) draw_func = getattr(data, f"draw_{choice_type}") draw_func(**constraints) assert tree.root.transition is None assert tree.root.choice_types == [choice_type] test() def _test_non_observed_draws_are_not_recorded_in_tree(choice_type): @given(constraints_strategy(choice_type)) def test(constraints): assume(compute_max_children(choice_type, constraints) > 1) tree = DataTree() data = fresh_data(observer=tree.new_observer()) draw_func = getattr(data, f"draw_{choice_type}") draw_func(**constraints, observe=False) root = tree.root assert root.transition is None assert root.constraints == root.values == root.choice_types == [] test() @pytest.mark.parametrize( "choice_type", ["integer", "float", "boolean", "string", "bytes"] ) def test_observed_choice_type_draw(choice_type): _test_observed_draws_are_recorded_in_tree(choice_type) @pytest.mark.parametrize( "choice_type", ["integer", "float", "boolean", "string", "bytes"] ) def test_non_observed_choice_type_draw(choice_type): _test_non_observed_draws_are_not_recorded_in_tree(choice_type) def test_can_generate_hard_values(): tree = DataTree() min_value = 0 max_value = 1000 # set up `tree` such that [0, 999] have been drawn and only n=1000 remains. for i in range(max_value): data = ConjectureData.for_choices([i], observer=tree.new_observer()) data.draw_integer(min_value, max_value) data.freeze() # this test doubles as conjecture coverage for using our child cache, so # ensure we don't miss that logic by getting lucky and drawing the correct # value once or twice. for _ in range(20): prefix = tree.generate_novel_prefix(Random()) data = ConjectureData.for_choices(prefix) assert data.draw_integer(min_value, max_value) == 1000 def test_can_generate_hard_floats(): # similar to test_can_generate_hard_values, but exercises float-specific # logic for handling e.g. 0.0 vs -0.0 as different keys. tree = DataTree() def next_up_n(f, n): for _ in range(n): f = next_up(f) return f min_value = -0.0 max_value = next_up_n(min_value, 100) for n in range(100): @run_to_nodes def nodes(data): f = next_up_n(min_value, n) data.draw_float(min_value, max_value, forced=f, allow_nan=False) data.mark_interesting(interesting_origin()) data = ConjectureData.for_choices( [n.value for n in nodes], observer=tree.new_observer() ) data.draw_float(min_value, max_value, allow_nan=False) data.freeze() # we have left out a single value, so we can assert that generate_novel_prefix # is equal to that value. # # this test doubles as conjecture coverage for drawing floats from the # children cache. Draw a few times to ensure we hit that logic (as opposed # to getting lucky and drawing the correct value the first time). for _ in range(20): expected_value = next_up_n(min_value, 100) prefix = tree.generate_novel_prefix(Random()) data = ConjectureData.for_choices(prefix) assert data.draw_float(min_value, max_value, allow_nan=False) == expected_value @given(boolean_constraints(), integer_constraints()) def test_datatree_repr(bool_constraints, int_constraints): tree = DataTree() origin = interesting_origin() observer = tree.new_observer() observer.draw_boolean(True, was_forced=False, constraints=bool_constraints) observer.conclude_test(Status.INVALID, interesting_origin=None) observer = tree.new_observer() observer.draw_boolean(False, was_forced=False, constraints=bool_constraints) observer.draw_integer(42, was_forced=False, constraints=int_constraints) observer.conclude_test(Status.VALID, interesting_origin=None) observer = tree.new_observer() observer.draw_boolean(False, was_forced=False, constraints=bool_constraints) observer.draw_integer(0, was_forced=False, constraints=int_constraints) observer.draw_boolean(False, was_forced=True, constraints=bool_constraints) observer.conclude_test(Status.INTERESTING, interesting_origin=origin) observer = tree.new_observer() observer.draw_boolean(False, was_forced=False, constraints=bool_constraints) observer.draw_integer(5, was_forced=False, constraints=int_constraints) assert ( pretty.pretty(tree) == textwrap.dedent( f""" boolean True {bool_constraints} Conclusion (Status.INVALID) boolean False {bool_constraints} integer 42 {int_constraints} Conclusion (Status.VALID) integer 0 {int_constraints} boolean False [forced] {bool_constraints} Conclusion (Status.INTERESTING, {origin}) integer 5 {int_constraints} unknown """ ).strip() ) def _draw(data, node, *, forced=None): return getattr(data, f"draw_{node.type}")(**node.constraints, forced=forced) @given(nodes(was_forced=True, choice_types=["float"])) def test_simulate_forced_floats(node): tree = DataTree() data = ConjectureData.for_choices([node.value], observer=tree.new_observer()) data.draw_float(**node.constraints, forced=node.value) with pytest.raises(StopTest): data.conclude_test(Status.VALID) data = ConjectureData.for_choices([node.value], observer=tree.new_observer()) tree.simulate_test_function(data) data.freeze() assert data.nodes == (node,) ================================================ FILE: hypothesis-python/tests/conjecture/test_engine.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import enum import re import time from random import Random from unittest.mock import Mock import pytest from hypothesis import ( HealthCheck, Phase, Verbosity, assume, given, settings, strategies as st, ) from hypothesis.database import ( InMemoryExampleDatabase, choices_from_bytes, choices_to_bytes, ) from hypothesis.errors import FailedHealthCheck, FlakyStrategyDefinition from hypothesis.internal.compat import PYPY, bit_count, int_from_bytes from hypothesis.internal.conjecture import engine as engine_module from hypothesis.internal.conjecture.data import ConjectureData, Overrun, Status from hypothesis.internal.conjecture.datatree import compute_max_children from hypothesis.internal.conjecture.engine import ( INVALID_PER_VALID, INVALID_THRESHOLD_BASE, MIN_TEST_CALLS, ConjectureRunner, ExitReason, HealthCheckState, RunIsComplete, ) from hypothesis.internal.conjecture.junkdrawer import startswith from hypothesis.internal.conjecture.pareto import DominanceRelation, dominance from hypothesis.internal.conjecture.shrinker import Shrinker, ShrinkPass from hypothesis.internal.coverage import IN_COVERAGE_TESTS from tests.common.debug import minimal from tests.common.strategies import SLOW, HardToShrink from tests.common.utils import no_shrink, skipif_time_unpatched from tests.conjecture.common import ( SOME_LABEL, buffer_size_limit, interesting_origin, nodes, run_to_nodes, shrinking_from, ) runner_settings = settings( max_examples=100, database=None, suppress_health_check=list(HealthCheck) ) def test_non_cloneable_intervals(): @run_to_nodes def nodes(data): data.draw_bytes(10, 10) data.draw_bytes(9, 9) data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (bytes(10), bytes(9)) def test_deletable_draws(): @run_to_nodes def nodes(data): while True: x = data.draw_bytes(2, 2) if x[0] == 255: data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (b"\xff\x00",) def test_can_load_data_from_a_corpus(): key = b"hi there" db = InMemoryExampleDatabase() value = b"=\xc3\xe4l\x81\xe1\xc2H\xc9\xfb\x1a\xb6bM\xa8\x7f" db.save(key, choices_to_bytes([value])) def f(data): if data.draw_bytes() == value: data.mark_interesting(interesting_origin()) runner = ConjectureRunner(f, settings=settings(database=db), database_key=key) runner.run() (last_data,) = runner.interesting_examples.values() assert last_data.choices == (value,) assert len(list(db.fetch(key))) == 1 def slow_shrinker(): strat = HardToShrink() def accept(data): if data.draw(strat): data.mark_interesting(interesting_origin()) return accept @pytest.mark.parametrize("n", [1, 5]) def test_terminates_shrinks(n, monkeypatch): db = InMemoryExampleDatabase() def generate_new_examples(self): self.cached_test_function((255,) * 1000) monkeypatch.setattr( ConjectureRunner, "generate_new_examples", generate_new_examples ) monkeypatch.setattr(engine_module, "MAX_SHRINKS", n) runner = ConjectureRunner( slow_shrinker(), settings=settings(max_examples=5000, database=db), random=Random(0), database_key=b"key", ) runner.run() (last_data,) = runner.interesting_examples.values() assert last_data.status == Status.INTERESTING assert runner.exit_reason == ExitReason.max_shrinks assert runner.shrinks == n in_db = set(db.data[runner.secondary_key]) assert len(in_db) == n def test_detects_flakiness(): failed_once = False count = 0 def tf(data): nonlocal count, failed_once data.draw_bytes(1, 1) count += 1 if not failed_once: failed_once = True data.mark_interesting(interesting_origin()) runner = ConjectureRunner(tf) runner.run() assert runner.exit_reason == ExitReason.flaky assert count == MIN_TEST_CALLS + 1 def recur(i, data): if i >= 1: recur(i - 1, data) @pytest.mark.skipif(PYPY, reason="stack tricks only work reliably on CPython") @pytest.mark.skipif( IN_COVERAGE_TESTS, reason="flaky under coverage instrumentation? see #4391" ) def test_recursion_error_is_not_flaky(): def tf(data): i = data.draw_integer(0, 2**16 - 1) try: recur(i, data) except RecursionError: data.mark_interesting(interesting_origin()) runner = ConjectureRunner(tf, settings=settings(derandomize=True)) runner.run() assert runner.exit_reason == ExitReason.finished def test_variadic_draw(): def draw_list(data): result = [] while True: data.start_span(SOME_LABEL) n = data.draw_integer(0, 2) if n: result.append(data.draw_bytes(n, n)) data.stop_span() if not n: break return result @run_to_nodes def nodes(data): if any(all(d) for d in draw_list(data)): data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (1, b"\x01", 0) def test_draw_to_overrun(): @run_to_nodes def nodes(data): d = (data.draw_bytes(1, 1)[0] - 8) & 0xFF data.draw_bytes(128 * d, 128 * d) if d >= 2: data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (bytes([10]),) + (bytes(128 * 2),) def test_can_navigate_to_a_valid_example(): def f(data): i = int_from_bytes(data.draw_bytes(2, 2)) data.draw_bytes(i, i) data.mark_interesting(interesting_origin()) runner = ConjectureRunner(f, settings=settings(max_examples=5000, database=None)) with buffer_size_limit(4): runner.run() assert runner.interesting_examples def test_stops_after_max_examples_when_reading(): key = b"key" db = InMemoryExampleDatabase() for i in range(10): db.save(key, bytes([i])) seen = [] def f(data): seen.append(data.draw_bytes(1, 1)) runner = ConjectureRunner( f, settings=settings(max_examples=1, database=db), database_key=key ) runner.run() assert len(seen) == 1 def test_stops_after_max_examples_when_generating(): seen = [] def f(data): seen.append(data.draw_bytes(1, 1)) runner = ConjectureRunner(f, settings=settings(max_examples=1, database=None)) runner.run() assert len(seen) == 1 @pytest.mark.parametrize("examples", [1, 5, 20, 50]) def test_stops_after_max_examples_when_generating_more_bugs(examples): seen = [] err_common = False err_rare = False def f(data): seen.append(data.draw_integer(0, 2**32 - 1)) # Rare, potentially multi-error conditions nonlocal err_common, err_rare if seen[-1] > 2**31: err_rare = True raise ValueError err_common = True raise Exception runner = ConjectureRunner( f, settings=settings(max_examples=examples, phases=[Phase.generate]) ) try: runner.run() except Exception: pass # No matter what, whether examples is larger or smalller than MAX_TEST_CALLS, # we stop looking at max_examples. (and re-run each failure for the traceback) assert len(seen) <= examples + err_common + err_rare def test_interleaving_engines(): children = [] @run_to_nodes def nodes(data): rnd = Random(data.draw_bytes(1, 1)) def g(d2): d2.draw_bytes(1, 1) data.mark_interesting(interesting_origin()) runner = ConjectureRunner(g, random=rnd) children.append(runner) runner.run() if runner.interesting_examples: data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (b"\0",) for c in children: assert not c.interesting_examples def test_phases_can_disable_shrinking(): seen = set() def f(data): seen.add(bytes(data.draw_bytes(32, 32))) data.mark_interesting(interesting_origin()) runner = ConjectureRunner( f, settings=settings(database=None, phases=(Phase.reuse, Phase.generate)) ) runner.run() assert len(seen) == 1 def test_reuse_phase_runs_for_max_examples_if_generation_is_disabled(): db = InMemoryExampleDatabase() for i in range(256): db.save(b"key", choices_to_bytes([i])) seen = set() def test(data): seen.add(data.draw_integer(0, 2**8 - 1)) ConjectureRunner( test, settings=settings(max_examples=100, database=db, phases=[Phase.reuse]), database_key=b"key", random=Random(0), ).run() assert len(seen) == 100 def test_erratic_draws(): n = 0 with pytest.raises(FlakyStrategyDefinition): @run_to_nodes def nodes(data): nonlocal n data.draw_bytes(n, n) data.draw_bytes(255 - n, 255 - n) if n == 255: data.mark_interesting(interesting_origin()) else: n += 1 def test_no_read_no_shrink(): count = 0 @run_to_nodes def nodes(data): nonlocal count count += 1 data.mark_interesting(interesting_origin()) assert nodes == () assert count == 1 def test_one_dead_branch(): seen = set() @run_to_nodes def nodes(data): i = data.draw_bytes(1, 1)[0] if i > 0: data.mark_invalid() i = data.draw_bytes(1, 1)[0] if len(seen) < 255: seen.add(i) elif i not in seen: data.mark_interesting(interesting_origin()) def test_does_not_save_on_interrupt(): def interrupts(data): raise KeyboardInterrupt db = InMemoryExampleDatabase() runner = ConjectureRunner( interrupts, settings=settings(database=db), database_key=b"key" ) with pytest.raises(KeyboardInterrupt): runner.run() assert not db.data def test_saves_on_skip_exceptions_to_reraise(): # skip exceptions should be saved to the db so we spend as little time as # possible exploring these tests in the future (if eg the skip is guarded # by a conditional that takes some time to hit). see also # https://github.com/HypothesisWorks/hypothesis/pull/4316#discussion_r2008912585 def raises(data): pytest.skip() db = InMemoryExampleDatabase() runner = ConjectureRunner( raises, settings=settings(database=db), database_key=b"key" ) with pytest.raises(pytest.skip.Exception): runner.run() assert len(db.data) == 1 def test_returns_forced(): value = b"\0\1\2\3" @run_to_nodes def nodes(data): data.draw_bytes(len(value), len(value), forced=value) data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (value,) def fails_health_check(label, **kwargs): def accept(f): runner = ConjectureRunner( f, settings=settings( max_examples=100, phases=no_shrink, database=None, **kwargs ), ) with pytest.raises(FailedHealthCheck) as e: runner.run() assert str(label) in str(e.value) assert not runner.interesting_examples return accept def test_fails_health_check_for_all_invalid(): @fails_health_check(HealthCheck.filter_too_much) def _(data): data.draw_bytes(2, 2) data.mark_invalid() def test_fails_health_check_for_large_base(): @fails_health_check(HealthCheck.large_base_example) def _(data): data.draw_bytes(10**6, 10**6) def test_fails_health_check_for_large_non_base(): @fails_health_check(HealthCheck.data_too_large) def _(data): if data.draw_boolean(): data.draw_bytes(10_000, 10_000) @skipif_time_unpatched def test_fails_health_check_for_slow_draws(): @fails_health_check(HealthCheck.too_slow) def _(data): data.draw(SLOW) def test_health_check_too_slow_with_invalid_examples(): @st.composite def s(draw): n = draw(st.integers()) if n > 0: assume(False) time.sleep(0.2) @given(s()) @settings(derandomize=True, max_examples=100) def test(x): pass with pytest.raises(FailedHealthCheck) as exc: test() assert str(HealthCheck.too_slow) in str(exc.value) assert re.search(r"\d+ invalid inputs", str(exc.value)) def test_health_check_too_slow_with_overrun_examples(): @st.composite def s(draw): n = draw(st.integers()) if n > 0: draw(st.binary(min_size=50)) assume(False) time.sleep(0.2) @given(s()) @settings(derandomize=True, max_examples=100) def test(x): pass with buffer_size_limit(10), pytest.raises(FailedHealthCheck) as exc: test() assert str(HealthCheck.too_slow) in str(exc.value) assert re.search( r"\d+ inputs which exceeded the maximum allowed entropy", str(exc.value) ) @pytest.mark.parametrize("n_large", [1, 5, 8, 15]) def test_can_shrink_variable_draws(n_large): target = 128 * n_large @st.composite def strategy(draw): n = draw(st.integers(0, 15)) return [draw(st.integers(0, 255)) for _ in range(n)] ints = minimal(strategy(), lambda ints: sum(ints) >= target) # should look like [4, 255, 255, 255] assert ints == [target % 255] + [255] * (len(ints) - 1) def test_can_shrink_variable_string_draws(): @st.composite def strategy(draw): n = draw(st.integers(min_value=0, max_value=20)) return draw(st.text(st.characters(codec="ascii"), min_size=n, max_size=n)) s = minimal(strategy(), lambda s: len(s) >= 10 and "a" in s) # TODO_BETTER_SHRINK: this should be # assert s == "0" * 9 + "a" # but we first shrink to having a single a at the end of the string and then # fail to apply our special case invalid logic when shrinking the min_size n, # because that logic removes from the end of the string (which fails our # precondition). assert re.match("0+a", s) def test_variable_size_string_increasing(): # coverage test for min_size increasing during shrinking (because the test # function inverts n). # ...except this currently overruns instead and misses that check. @st.composite def strategy(draw): n = 10 - draw(st.integers(0, 10)) return draw(st.text(st.characters(codec="ascii"), min_size=n, max_size=n)) s = minimal(strategy(), lambda s: len(s) >= 5 and "a" in s) # TODO_BETTER_SHRINK: this should be # assert s == "0000a" # but instead shrinks to 00000000a. assert re.match("0+a", s) def test_run_nothing(): def f(data): raise AssertionError runner = ConjectureRunner(f, settings=settings(phases=())) runner.run() assert runner.call_count == 0 class Foo: def __repr__(self): return "stuff" def test_debug_data(capsys): choices = (0, 1, 2) def f(data): for choice in choices: if data.draw(st.integers(0, 100)) != choice: data.mark_invalid() data.start_span(1) data.stop_span() data.mark_interesting(interesting_origin()) runner = ConjectureRunner( f, settings=settings( max_examples=5000, database=None, suppress_health_check=list(HealthCheck), verbosity=Verbosity.debug, ), ) runner.cached_test_function(choices) runner.run() out, _ = capsys.readouterr() assert re.match(r"\d+ choices -> ", out) assert "INTERESTING" in out def test_can_write_bytes_towards_the_end(): buf = b"\1\2\3" def f(data): if data.draw_boolean(): data.draw_bytes(5, 5) data.draw_bytes(len(buf), len(buf), forced=buf) assert data.choices[2] == buf with buffer_size_limit(15): ConjectureRunner(f).run() def test_uniqueness_is_preserved_when_writing_at_beginning(): seen = set() def f(data): data.draw_bytes(1, 1, forced=bytes(1)) n = data.draw_integer(0, 2**3 - 1) assert n not in seen seen.add(n) runner = ConjectureRunner(f, settings=settings(max_examples=50)) runner.run() assert runner.valid_examples == len(seen) @pytest.mark.parametrize("skip_target", [False, True]) @pytest.mark.parametrize("initial_attempt", [(127,), (128,)]) def test_clears_out_its_database_on_shrinking( initial_attempt, skip_target, monkeypatch ): def generate_new_examples(self): self.cached_test_function(initial_attempt) monkeypatch.setattr( ConjectureRunner, "generate_new_examples", generate_new_examples ) key = b"key" db = InMemoryExampleDatabase() def f(data): if data.draw_integer() >= 127: data.mark_interesting(interesting_origin()) runner = ConjectureRunner( f, settings=settings(database=db, max_examples=256), database_key=key, random=Random(0), ) for n in range(256): if n != 127 or not skip_target: db.save(runner.secondary_key, choices_to_bytes([n])) runner.run() assert len(runner.interesting_examples) == 1 for b in db.fetch(runner.secondary_key): assert choices_from_bytes(b)[0] >= 127 assert len(list(db.fetch(runner.database_key))) == 1 def test_shrinks_both_interesting_examples(monkeypatch): def generate_new_examples(self): self.cached_test_function((1,)) monkeypatch.setattr( ConjectureRunner, "generate_new_examples", generate_new_examples ) def f(data): n = data.draw_integer(0, 2**8 - 1) data.mark_interesting(interesting_origin(n & 1)) runner = ConjectureRunner(f) runner.run() assert runner.interesting_examples[interesting_origin(0)].choices == (0,) assert runner.interesting_examples[interesting_origin(1)].choices == (1,) def test_discarding(monkeypatch): monkeypatch.setattr(Shrinker, "shrink", Shrinker.remove_discarded) monkeypatch.setattr( ConjectureRunner, "generate_new_examples", lambda runner: runner.cached_test_function((False, True) * 10), ) @run_to_nodes def nodes(data): count = 0 while count < 10: data.start_span(SOME_LABEL) b = data.draw_boolean() if b: count += 1 data.stop_span(discard=not b) data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (True,) * 10 def test_can_remove_discarded_data(): @shrinking_from((0,) * 10 + (11,)) def shrinker(data: ConjectureData): while True: data.start_span(SOME_LABEL) b = data.draw_integer(0, 2**8 - 1) data.stop_span(discard=(b == 0)) if b == 11: break data.mark_interesting(interesting_origin()) shrinker.remove_discarded() assert shrinker.choices == (11,) def test_discarding_iterates_to_fixed_point(): @shrinking_from(list(range(100, -1, -1))) def shrinker(data: ConjectureData): data.start_span(0) data.draw_integer(0, 2**8 - 1) data.stop_span(discard=True) while data.draw_integer(0, 2**8 - 1): pass data.mark_interesting(interesting_origin()) shrinker.remove_discarded() assert shrinker.choices == (1, 0) def test_discarding_is_not_fooled_by_empty_discards(): @shrinking_from((1, 1)) def shrinker(data: ConjectureData): data.draw_integer(0, 2**1 - 1) data.start_span(0) data.stop_span(discard=True) data.draw_integer(0, 2**1 - 1) data.mark_interesting(interesting_origin()) shrinker.remove_discarded() assert shrinker.shrink_target.has_discards def test_discarding_can_fail(): @shrinking_from((1,)) def shrinker(data: ConjectureData): data.start_span(0) data.draw_boolean() data.stop_span(discard=True) data.mark_interesting(interesting_origin()) shrinker.remove_discarded() assert any(e.discarded and e.choice_count > 0 for e in shrinker.shrink_target.spans) def test_shrinking_from_mostly_zero(monkeypatch): monkeypatch.setattr( ConjectureRunner, "generate_new_examples", lambda self: self.cached_test_function((0,) * 5 + (2,)), ) @run_to_nodes def nodes(data): s = [data.draw_integer(0, 2**8 - 1) for _ in range(6)] if any(s): data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (0,) * 5 + (1,) def test_handles_nesting_of_discard_correctly(monkeypatch): monkeypatch.setattr(Shrinker, "shrink", Shrinker.remove_discarded) monkeypatch.setattr( ConjectureRunner, "generate_new_examples", lambda runner: runner.cached_test_function((False, False, True, True)), ) @run_to_nodes def nodes(data): while True: data.start_span(SOME_LABEL) succeeded = data.draw_boolean() data.start_span(SOME_LABEL) data.draw_boolean() data.stop_span(discard=not succeeded) data.stop_span(discard=not succeeded) if succeeded: data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (True, True) def test_database_clears_secondary_key(): key = b"key" database = InMemoryExampleDatabase() def f(data): if data.draw_integer() == 10: data.mark_interesting(interesting_origin()) else: data.mark_invalid() runner = ConjectureRunner( f, settings=settings( max_examples=1, database=database, suppress_health_check=list(HealthCheck) ), database_key=key, ) for i in range(10): database.save(runner.secondary_key, choices_to_bytes([i])) runner.cached_test_function((10,)) assert runner.interesting_examples assert len(set(database.fetch(key))) == 1 assert len(set(database.fetch(runner.secondary_key))) == 10 runner.clear_secondary_key() assert len(set(database.fetch(key))) == 1 assert len(set(database.fetch(runner.secondary_key))) == 0 def test_database_uses_values_from_secondary_key(): key = b"key" database = InMemoryExampleDatabase() def f(data): if data.draw_integer() >= 5: data.mark_interesting(interesting_origin()) else: data.mark_invalid() runner = ConjectureRunner( f, settings=settings( max_examples=1, database=database, suppress_health_check=list(HealthCheck) ), database_key=key, ) for i in range(10): database.save(runner.secondary_key, choices_to_bytes([i])) runner.cached_test_function((10,)) assert runner.interesting_examples assert len(set(database.fetch(key))) == 1 assert len(set(database.fetch(runner.secondary_key))) == 10 runner.clear_secondary_key() assert len(set(database.fetch(key))) == 1 assert { choices_from_bytes(b)[0] for b in database.fetch(runner.secondary_key) } == set(range(6, 11)) (v,) = runner.interesting_examples.values() assert v.choices == (5,) def test_exit_because_max_iterations(): def f(data): data.draw_integer(0, 2**64 - 1) data.mark_invalid() runner = ConjectureRunner( f, settings=settings( max_examples=1, database=None, suppress_health_check=list(HealthCheck) ), ) runner.run() assert runner.call_count <= 1000 assert runner.exit_reason == ExitReason.max_iterations def test_max_iterations_with_all_invalid(): # With assume(False) on every example, we stop after INVALID_THRESHOLD_BASE + 1 # invalid attempts (the check is > not >=). def f(data): data.draw_integer(0, 2**64 - 1) data.mark_invalid() runner = ConjectureRunner( f, settings=settings( max_examples=10_000, database=None, suppress_health_check=list(HealthCheck) ), ) runner.run() assert runner.call_count == INVALID_THRESHOLD_BASE + 1 assert runner.exit_reason == ExitReason.max_iterations @pytest.mark.parametrize("n_valid", [1, 2, 5]) def test_max_iterations_with_some_valid(n_valid): valid_count = 0 def f(data): nonlocal valid_count data.draw_integer(0, 2**64 - 1) if valid_count < n_valid: valid_count += 1 else: data.mark_invalid() runner = ConjectureRunner( f, settings=settings( max_examples=10_000, database=None, suppress_health_check=list(HealthCheck) ), ) runner.run() assert ( runner.call_count == n_valid + INVALID_THRESHOLD_BASE + n_valid * INVALID_PER_VALID + 1 ) assert runner.exit_reason == ExitReason.max_iterations def test_exit_because_shrink_phase_timeout(monkeypatch): val = 0 def fast_time(): nonlocal val val += 1000 return val def f(data): if data.draw_integer(0, 2**64 - 1) > 2**33: data.mark_interesting(interesting_origin()) monkeypatch.setattr(time, "perf_counter", fast_time) runner = ConjectureRunner(f, settings=settings(database=None, max_examples=100_000)) runner.run() assert runner.exit_reason == ExitReason.very_slow_shrinking assert runner.statistics["stopped-because"] == "shrinking was very slow" def test_dependent_block_pairs_can_lower_to_zero(): @shrinking_from((True, 1)) def shrinker(data: ConjectureData): if data.draw_boolean(): n = data.draw_integer(0, 2**16 - 1) else: n = data.draw_integer(0, 2**8 - 1) if n == 1: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_individual_choices)]) assert shrinker.choices == (False, 1) def test_handle_size_too_large_during_dependent_lowering(): @shrinking_from((True, 255, 0)) def shrinker(data: ConjectureData): if data.draw_boolean(): data.draw_integer(0, 2**16 - 1) data.mark_interesting(interesting_origin()) else: data.draw_integer(0, 2**8 - 1) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_individual_choices)]) def test_block_may_grow_during_lexical_shrinking(): @shrinking_from((2, 1, 1)) def shrinker(data: ConjectureData): n = data.draw_integer(0, 2**8 - 1) if n == 2: data.draw_integer(0, 2**8 - 1) data.draw_integer(0, 2**8 - 1) else: data.draw_integer(0, 2**16 - 1) data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_individual_choices)]) assert shrinker.choices == (0, 0) def test_lower_common_node_offset_does_nothing_when_changed_blocks_are_zero(): @shrinking_from((True, False, True, False)) def shrinker(data: ConjectureData): data.draw_boolean() data.draw_boolean() data.draw_boolean() data.draw_boolean() data.mark_interesting(interesting_origin()) shrinker.mark_changed(1) shrinker.mark_changed(3) shrinker.lower_common_node_offset() assert shrinker.choices == (True, False, True, False) def test_lower_common_node_offset_ignores_zeros(): @shrinking_from((2, 2, 0)) def shrinker(data: ConjectureData): n = data.draw_integer(0, 2**8 - 1) data.draw_integer(0, 2**8 - 1) data.draw_integer(0, 2**8 - 1) if n > 0: data.mark_interesting(interesting_origin()) for i in range(3): shrinker.mark_changed(i) shrinker.lower_common_node_offset() assert shrinker.choices == (1, 1, 0) def test_cached_test_function_returns_right_value(): count = 0 def tf(data): nonlocal count count += 1 data.draw_integer(0, 3) data.mark_interesting(interesting_origin()) runner = ConjectureRunner(tf, settings=runner_settings, random=Random(0)) for _ in range(2): for choices in ((0,), (1,)): d = runner.cached_test_function(choices) assert d.status == Status.INTERESTING assert d.choices == choices assert count == 2 def test_cached_test_function_does_not_reinvoke_on_prefix(): call_count = 0 def test_function(data): nonlocal call_count call_count += 1 data.draw_integer(0, 2**8 - 1) data.draw_bytes(1, 1, forced=bytes([7])) data.draw_integer(0, 2**8 - 1) runner = ConjectureRunner(test_function, settings=runner_settings, random=Random(0)) data = runner.cached_test_function((0, b"\0", 0)) assert data.status == Status.VALID for n in [2, 1, 0]: d = runner.cached_test_function(data.choices[:n]) assert d is Overrun assert call_count == 1 def test_will_evict_entries_from_the_cache(monkeypatch): monkeypatch.setattr(engine_module, "CACHE_SIZE", 5) count = 0 def tf(data): nonlocal count data.draw_integer(0, 2**8 - 1) count += 1 runner = ConjectureRunner(tf, settings=runner_settings) for _ in range(3): for n in range(10): runner.cached_test_function((n,)) # Because we exceeded the cache size, our previous # calls will have been evicted, so each call to # cached_test_function will have to reexecute. assert count == 30 def test_branch_ending_in_write(): seen = set() def tf(data): count = 0 while data.draw_boolean(): count += 1 if count > 1: data.draw_boolean(forced=False) assert data.nodes not in seen seen.add(data.nodes) # max_examples high enough that our for loop below won't hit it runner = ConjectureRunner( tf, settings=settings(runner_settings, max_examples=200), random=Random(0) ) for _ in range(100): prefix = runner.generate_novel_prefix() attempt = prefix + (False, False) data = runner.cached_test_function(attempt) assert data.status is Status.VALID assert startswith(attempt, data.choices) def test_exhaust_space(): runner = ConjectureRunner( lambda data: data.draw_boolean(), settings=runner_settings, random=Random(0) ) runner.run() assert runner.tree.is_exhausted assert runner.valid_examples == 2 SMALL_COUNT_SETTINGS = settings(runner_settings, max_examples=500) def test_discards_kill_branches(): seen = set() def test(data: ConjectureData): data.start_span(1) n1 = data.draw_integer(0, 9) data.stop_span(discard=n1 > 0) n2 = data.draw_integer(0, 9) n3 = data.draw_integer(0, 9) assert (n1, n2, n3) not in seen seen.add((n1, n2, n3)) runner = ConjectureRunner(test, settings=SMALL_COUNT_SETTINGS) runner.run() assert runner.exit_reason is ExitReason.finished # 10 to explore the initial n1 = 0-9 statespace, then 100 to explore just # the (0, n1, n2) statespace, because every prefix other than 0 is a discard # and is not explored. Subtract the overlap, which occurs once at n1 = 0. assert len(seen) == 100 + 10 - 1 @pytest.mark.parametrize("n", range(1, 32)) def test_number_of_examples_in_integer_range_is_bounded(n): def test(data): assert runner.call_count <= 2 * n data.draw_integer(0, n) runner = ConjectureRunner(test, settings=SMALL_COUNT_SETTINGS, random=Random(0)) runner.run() def test_prefix_cannot_exceed_buffer_size(monkeypatch): buffer_size = 10 with buffer_size_limit(buffer_size): def test(data): while data.draw_boolean(): assert data.length <= buffer_size assert data.length <= buffer_size runner = ConjectureRunner(test, settings=SMALL_COUNT_SETTINGS, random=Random(0)) runner.run() assert runner.valid_examples == buffer_size def test_does_not_shrink_multiple_bugs_when_told_not_to(): def test(data): m = data.draw_integer(0, 2**8 - 1) n = data.draw_integer(0, 2**8 - 1) if m > 0: data.mark_interesting(interesting_origin(1)) if n > 5: data.mark_interesting(interesting_origin(2)) runner = ConjectureRunner( test, settings=settings(runner_settings, report_multiple_bugs=False), random=Random(0), ) runner.cached_test_function((255, 255)) runner.shrink_interesting_examples() results = {d.choices for d in runner.interesting_examples.values()} assert len(results.intersection({(0, 1), (1, 0)})) == 1 def test_does_not_keep_generating_when_multiple_bugs(): def test(data): if data.draw_integer(0, 2**20 - 1) > 0: data.draw_integer(0, 2**20 - 1) data.mark_interesting(interesting_origin()) runner = ConjectureRunner( test, settings=settings( runner_settings, report_multiple_bugs=False, phases=[Phase.generate] ), random=Random(0), ) runner.run() assert runner.call_count == 2 def test_shrink_after_max_examples(): """If we find a bug, keep looking for more, and then hit the valid-example limit, we should still proceed to shrinking. """ max_examples = 100 fail_at = max_examples - 5 seen = set() bad = set() post_failure_calls = [0] def test(data): if bad: post_failure_calls[0] += 1 value = data.draw_integer(0, 2**8 - 1) if value in seen and value not in bad: return seen.add(value) if len(seen) == fail_at: bad.add(value) if value in bad: data.mark_interesting(interesting_origin()) runner = ConjectureRunner( test, settings=settings( runner_settings, max_examples=max_examples, phases=[Phase.generate, Phase.shrink], report_multiple_bugs=True, ), # This shouldn't need to be deterministic, but it makes things much easier # to debug if anything goes wrong. random=Random(0), ) runner.shrink_interesting_examples = Mock(name="shrink_interesting_examples") runner.run() # First, verify our test assumptions: we found a bug, kept running, and # then hit max-examples. assert runner.interesting_examples assert post_failure_calls[0] >= (max_examples - fail_at) assert runner.call_count >= max_examples assert runner.valid_examples == max_examples # Now check that we still performed shrinking, even after hitting the # example limit. assert runner.shrink_interesting_examples.call_count == 1 assert runner.exit_reason == ExitReason.finished def test_shrink_after_max_iterations(): """If we find a bug, keep looking for more, and then hit the invalid examples limit, we should still proceed to shrinking. """ max_examples = 10 max_iterations = INVALID_THRESHOLD_BASE fail_at = max_iterations - 5 invalid = set() bad = set() post_failure_calls = [0] def test(data): if bad: post_failure_calls[0] += 1 value = data.draw_integer(0, 2**16 - 1) if value in invalid: data.mark_invalid() if value in bad or (not bad and len(invalid) == fail_at): bad.add(value) data.mark_interesting(interesting_origin()) invalid.add(value) data.mark_invalid() # This shouldn't need to be deterministic, but it makes things much easier # to debug if anything goes wrong. runner = ConjectureRunner( test, settings=settings( runner_settings, max_examples=max_examples, phases=[Phase.generate, Phase.shrink], report_multiple_bugs=True, ), random=Random(0), ) runner.shrink_interesting_examples = Mock(name="shrink_interesting_examples") runner.run() # First, verify our test assumptions: we found a bug, kept running, and # then hit the test call limit. assert runner.interesting_examples assert post_failure_calls[0] >= (max_iterations - fail_at) - 1 assert runner.call_count >= max_iterations assert runner.valid_examples == 0 # Now check that we still performed shrinking, even after hitting the # test call limit. assert runner.shrink_interesting_examples.call_count == 1 assert runner.exit_reason == ExitReason.finished def test_populates_the_pareto_front(): def test(data): data.target_observations[""] = data.draw_integer(0, 2**4 - 1) runner = ConjectureRunner( test, settings=settings( max_examples=5000, database=InMemoryExampleDatabase(), suppress_health_check=list(HealthCheck), ), database_key=b"stuff", random=Random(0), ) runner.run() assert len(runner.pareto_front) == 2**4 def test_pareto_front_contains_smallest_valid(): def test(data): data.target_observations[""] = 1 data.draw_integer(0, 2**4 - 1) runner = ConjectureRunner( test, settings=settings( max_examples=5000, database=InMemoryExampleDatabase(), suppress_health_check=list(HealthCheck), ), database_key=b"stuff", random=Random(0), ) runner.run() assert len(runner.pareto_front) == 1 def test_replaces_all_dominated(): def test(data): data.target_observations["m"] = 3 - data.draw_integer(0, 3) data.target_observations["n"] = 3 - data.draw_integer(0, 3) runner = ConjectureRunner( test, settings=settings(runner_settings, database=InMemoryExampleDatabase()), database_key=b"stuff", ) d1 = runner.cached_test_function((0, 1)).as_result() d2 = runner.cached_test_function((1, 0)).as_result() assert len(runner.pareto_front) == 2 assert runner.pareto_front[0] == d1 assert runner.pareto_front[1] == d2 d3 = runner.cached_test_function((0, 0)).as_result() assert len(runner.pareto_front) == 1 assert runner.pareto_front[0] == d3 def test_does_not_duplicate_elements(): def test(data): data.target_observations["m"] = data.draw_integer(0, 2**8 - 1) runner = ConjectureRunner( test, settings=settings(runner_settings, database=InMemoryExampleDatabase()), database_key=b"stuff", ) d1 = runner.cached_test_function((1,)).as_result() assert len(runner.pareto_front) == 1 # This can happen in practice if we e.g. reexecute a test because it has # expired from the cache. It's easier just to test it directly though # rather than simulate the failure mode. assert runner.pareto_front.add(d1) assert len(runner.pareto_front) == 1 def test_includes_right_hand_side_targets_in_dominance(): def test(data): if data.draw_integer(0, 2**8 - 1): data.target_observations[""] = 10 runner = ConjectureRunner( test, settings=settings(runner_settings, database=InMemoryExampleDatabase()), database_key=b"stuff", ) d1 = runner.cached_test_function((0,)).as_result() d2 = runner.cached_test_function((1,)).as_result() assert dominance(d1, d2) == DominanceRelation.NO_DOMINANCE def test_smaller_interesting_dominates_larger_valid(): def test(data): if data.draw_integer(0, 2**8 - 1) == 0: data.mark_interesting(interesting_origin()) runner = ConjectureRunner( test, settings=settings(runner_settings, database=InMemoryExampleDatabase()), database_key=b"stuff", ) d1 = runner.cached_test_function((0,)).as_result() d2 = runner.cached_test_function((1,)).as_result() assert dominance(d1, d2) == DominanceRelation.LEFT_DOMINATES def test_runs_full_set_of_examples(): def test(data): data.draw_integer(0, 2**64 - 1) runner = ConjectureRunner( test, settings=settings(runner_settings, database=InMemoryExampleDatabase()), database_key=b"stuff", ) runner.run() assert runner.valid_examples == runner_settings.max_examples def test_runs_optimisation_even_if_not_generating(): def test(data): data.target_observations["n"] = data.draw_integer(0, 2**16 - 1) runner = ConjectureRunner( test, settings=settings(runner_settings, phases=[Phase.target]), random=Random(0), ) runner.cached_test_function((0,)) runner.run() assert runner.best_observed_targets["n"] == (2**16) - 1 def test_runs_optimisation_once_when_generating(): def test(data): data.target_observations["n"] = data.draw_integer(0, 2**16 - 1) runner = ConjectureRunner( test, settings=settings(runner_settings, max_examples=100), random=Random(0) ) runner.optimise_targets = Mock(name="optimise_targets") try: runner.generate_new_examples() except RunIsComplete: pass assert runner.optimise_targets.call_count == 1 def test_does_not_run_optimisation_when_max_examples_is_small(): def test(data): data.target_observations["n"] = data.draw_integer(0, 2**16 - 1) runner = ConjectureRunner( test, settings=settings(runner_settings, max_examples=10), random=Random(0) ) runner.optimise_targets = Mock(name="optimise_targets") try: runner.generate_new_examples() except RunIsComplete: pass assert runner.optimise_targets.call_count == 0 def test_does_not_cache_extended_prefix(): def test(data): data.draw_integer() data.draw_integer() runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) d1 = runner.cached_test_function((0,), extend=10) assert runner.call_count == 1 d2 = runner.cached_test_function((0,), extend=10) assert runner.call_count == 2 assert d1.status is d2.status is Status.VALID def test_does_cache_if_extend_is_not_used(): calls = 0 def test(data): nonlocal calls calls += 1 data.draw_bytes(1, 1) runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) d1 = runner.cached_test_function((b"\0"), extend=8) d2 = runner.cached_test_function((b"\0"), extend=8) assert d1.status == d2.status == Status.VALID assert d1.choices == d2.choices assert calls == 1 def test_does_result_for_reuse(): calls = 0 def test(data): nonlocal calls calls += 1 data.draw_bytes(1, 1) runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) d1 = runner.cached_test_function((b"\0"), extend=8) d2 = runner.cached_test_function(d1.choices) assert d1.status == d2.status == Status.VALID assert d1.nodes == d2.nodes assert calls == 1 def test_does_not_use_cached_overrun_if_extending(): def test(data): data.draw_integer() data.draw_integer() runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) data = runner.cached_test_function((1,)) assert data.status == Status.OVERRUN assert runner.call_count == 1 # the choice sequence of (1,) maps to an overrun in the cache, but we # do not want to use this cache entry if we're extending. data = runner.cached_test_function((1,), extend=1) assert data.status == Status.VALID assert runner.call_count == 2 def test_uses_cached_overrun_if_not_extending(): def test(data): data.draw_integer() data.draw_integer() runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) data = runner.cached_test_function((1,), extend=0) assert data.status is Status.OVERRUN assert runner.call_count == 1 data = runner.cached_test_function((1,), extend=0) assert data.status is Status.OVERRUN assert runner.call_count == 1 def test_can_be_set_to_ignore_limits(): def test(data): data.draw_integer(0, 2**8 - 1) runner = ConjectureRunner( test, settings=settings(runner_settings, max_examples=1), ignore_limits=True, random=Random(0), ) for c in range(256): runner.cached_test_function((c,)) assert runner.tree.is_exhausted def test_too_slow_report(): state = HealthCheckState() assert state.timing_report() == "" # no draws recorded -> no report state.draw_times = { "generate:a": [2.0, 0.356789, 0.0], "generate:b": [0.1111111, 0.0, 0.002345678, 0.05, 0.123456, 0.1, 0.1, 0.1], "generate:c": [0.03, 0.05, 0.2], "generate:d": [0.04], "generate:e": [0.05, 0.01], "generate:f": [0.06], "generate:g": [0.07], "generate:h": [0.08], "generate:i": [0.09, 0.00001], } expected = """ count | fraction | slowest draws (seconds) a | 3 | 65% | -- -- -- 0.357, 2.000 b | 8 | 16% | 0.100, 0.100, 0.100, 0.111, 0.123 c | 3 | 8% | -- -- 0.030, 0.050, 0.200 i | 2 | 2% | -- -- -- -- 0.090 h | 1 | 2% | -- -- -- -- 0.080 (skipped 4 rows of fast draws)""" got = state.timing_report() print(got) assert expected == got def _draw(cd, node): return getattr(cd, f"draw_{node.type}")(**node.constraints) @given(nodes(was_forced=False)) def test_overruns_with_extend_are_not_cached(node): assume(compute_max_children(node.type, node.constraints) > 100) def test(cd): _draw(cd, node) _draw(cd, node) runner = ConjectureRunner(test) assert runner.call_count == 0 data = runner.cached_test_function([node.value]) assert runner.call_count == 1 assert data.status is Status.OVERRUN # cache hit data = runner.cached_test_function([node.value]) assert runner.call_count == 1 assert data.status is Status.OVERRUN # cache miss data = runner.cached_test_function([node.value], extend="full") assert runner.call_count == 2 assert data.status is Status.VALID def test_simulate_to_evicted_data(monkeypatch): # test that we do not rely on the false invariant that correctly simulating # a data to a result means we have that result in the cache, due to e.g. # cache evictions (but also potentially other trickery). monkeypatch.setattr(engine_module, "CACHE_SIZE", 1) def test(data): data.draw_integer() runner = ConjectureRunner(test) runner.cached_test_function([0]) # cache size is 1 so this evicts [0] runner.cached_test_function([1]) assert runner.call_count == 2 # we dont throw PreviouslyUnseenBehavior when simulating, but the result # was evicted to the cache so we will still call through to the test function. runner.tree.simulate_test_function(ConjectureData.for_choices([0])) runner.cached_test_function([0]) assert runner.call_count == 3 @pytest.mark.parametrize( "strategy, condition", [ (st.lists(st.integers(), min_size=5), lambda v: True), (st.lists(st.text(), min_size=2, unique=True), lambda v: True), ( st.sampled_from( enum.Flag("LargeFlag", {f"bit{i}": enum.auto() for i in range(64)}) ), lambda f: bit_count(f.value) > 1, ), ], ) def test_mildly_complicated_strategies(strategy, condition): # There are some code paths in engine.py and shrinker.py that are easily # covered by shrinking any mildly compliated strategy and aren't worth # testing explicitly for. This covers those. minimal(strategy, condition) def test_does_not_shrink_if_replaying_from_database(): db = InMemoryExampleDatabase() key = b"foo" def f(data): if data.draw_integer(0, 255) == 123: data.mark_interesting(interesting_origin()) runner = ConjectureRunner(f, settings=settings(database=db), database_key=key) choices = (123,) runner.save_choices(choices) runner.shrink_interesting_examples = None runner.run() (last_data,) = runner.interesting_examples.values() assert last_data.choices == choices def test_does_shrink_if_replaying_inexact_from_database(): db = InMemoryExampleDatabase() key = b"foo" def f(data): data.draw_integer(0, 255) data.mark_interesting(interesting_origin()) runner = ConjectureRunner(f, settings=settings(database=db), database_key=key) runner.save_choices((123, 2)) runner.run() (last_data,) = runner.interesting_examples.values() assert last_data.choices == (0,) def test_stops_if_hits_interesting_early_and_only_want_one_bug(): db = InMemoryExampleDatabase() key = b"foo" def f(data): data.draw_integer(0, 255) data.mark_interesting(interesting_origin()) runner = ConjectureRunner( f, settings=settings(database=db, report_multiple_bugs=False), database_key=key ) for i in range(256): runner.save_choices([i]) runner.run() assert runner.call_count == 1 def test_skips_secondary_if_interesting_is_found(): db = InMemoryExampleDatabase() key = b"foo" def f(data): data.draw_integer(0, 255) data.mark_interesting(interesting_origin()) runner = ConjectureRunner( f, settings=settings(max_examples=1000, database=db, report_multiple_bugs=True), database_key=key, ) for i in range(256): db.save( runner.database_key if i < 10 else runner.secondary_key, choices_to_bytes([i]), ) runner.reuse_existing_examples() assert runner.call_count == 10 @pytest.mark.parametrize("key_name", ["database_key", "secondary_key"]) def test_discards_invalid_db_entries(key_name): def test(data): data.draw_integer() data.mark_interesting(interesting_origin()) db = InMemoryExampleDatabase() runner = ConjectureRunner( test, # stop IN_COVERAGE_TESTS from overriding max_examples, which changes # db behavior settings=settings(database=db, max_examples=100), database_key=b"stuff", random=Random(0), ) key = getattr(runner, key_name) valid = choices_to_bytes([1]) db.save(key, valid) for n in range(5): b = bytes([255, n]) # save a bunch of invalid entries under the database key assert choices_from_bytes(b) is None db.save(key, b) assert len(set(db.fetch(key))) == 6 # this will clear out the invalid entries and use the valid one runner.reuse_existing_examples() runner.clear_secondary_key() assert set(db.fetch(runner.database_key)) == {valid} assert runner.call_count == 1 def test_discards_invalid_db_entries_pareto(): def test(data): data.draw_integer() data.mark_interesting(interesting_origin()) db = InMemoryExampleDatabase() runner = ConjectureRunner( test, settings=settings(database=db, max_examples=100), database_key=b"stuff", random=Random(0), ) for n in range(5): b = bytes([255, n]) assert choices_from_bytes(b) is None db.save(runner.pareto_key, b) assert len(set(db.fetch(runner.pareto_key))) == 5 runner.reuse_existing_examples() assert not set(db.fetch(runner.database_key)) assert not set(db.fetch(runner.pareto_key)) assert runner.call_count == 0 ================================================ FILE: hypothesis-python/tests/conjecture/test_float_encoding.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys import pytest from hypothesis import HealthCheck, assume, example, given, settings, strategies as st from hypothesis.internal.compat import ceil, extract_bits, floor from hypothesis.internal.conjecture import floats as flt from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.floats import SIGNALING_NAN, float_to_int from tests.conjecture.common import interesting_origin EXPONENTS = list(range(flt.MAX_EXPONENT + 1)) assert len(EXPONENTS) == 2**11 def assert_reordered_exponents(res): res = list(res) assert len(res) == len(EXPONENTS) for x in res: assert res.count(x) == 1 assert 0 <= x <= flt.MAX_EXPONENT def test_encode_permutes_elements(): assert_reordered_exponents(map(flt.encode_exponent, EXPONENTS)) def test_decode_permutes_elements(): assert_reordered_exponents(map(flt.decode_exponent, EXPONENTS)) def test_decode_encode(): for e in EXPONENTS: assert flt.decode_exponent(flt.encode_exponent(e)) == e def test_encode_decode(): for e in EXPONENTS: assert flt.decode_exponent(flt.encode_exponent(e)) == e @given(st.data()) def test_double_reverse_bounded(data): n = data.draw(st.integers(1, 64)) i = data.draw(st.integers(0, 2**n - 1)) j = flt.reverse_bits(i, n) assert flt.reverse_bits(j, n) == i @given(st.integers(0, 2**64 - 1)) def test_double_reverse(i): j = flt.reverse64(i) assert flt.reverse64(j) == i @example(0.0) @example(2.5) @example(8.000000000000007) @example(3.0) @example(2.0) @example(1.9999999999999998) @example(1.0) @given(st.floats(min_value=0.0)) def test_floats_round_trip(f): i = flt.float_to_lex(f) g = flt.lex_to_float(i) assert float_to_int(f) == float_to_int(g) @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much]) @example(1, 0.5) @given(st.integers(1, 2**53), st.floats(0, 1).filter(lambda x: x not in (0, 1))) def test_floats_order_worse_than_their_integral_part(n, g): f = n + g assume(int(f) != f) assume(int(f) != 0) i = flt.float_to_lex(f) if f < 0: g = ceil(f) else: g = floor(f) assert flt.float_to_lex(float(g)) < i integral_floats = st.floats(allow_infinity=False, allow_nan=False, min_value=0.0).map( lambda x: abs(float(int(x))) ) @given(integral_floats, integral_floats) def test_integral_floats_order_as_integers(x, y): assume(x != y) x, y = sorted((x, y)) assert flt.float_to_lex(x) < flt.float_to_lex(y) @given(st.floats(0, 1)) def test_fractional_floats_are_worse_than_one(f): assume(0 < f < 1) assert flt.float_to_lex(f) > flt.float_to_lex(1) def test_reverse_bits_table_reverses_bits(): for i, b in enumerate(flt.REVERSE_BITS_TABLE): assert extract_bits(i, width=8) == list(reversed(extract_bits(b, width=8))) def test_reverse_bits_table_has_right_elements(): assert sorted(flt.REVERSE_BITS_TABLE) == list(range(256)) def float_runner(start, condition, *, constraints=None): constraints = {} if constraints is None else constraints def test_function(data): f = data.draw_float(**constraints) if condition(f): data.mark_interesting(interesting_origin()) runner = ConjectureRunner(test_function) runner.cached_test_function((float(start),)) assert runner.interesting_examples return runner def minimal_from(start, condition, *, constraints=None): runner = float_runner(start, condition, constraints=constraints) runner.shrink_interesting_examples() (v,) = runner.interesting_examples.values() f = v.choices[0] assert condition(f) return f INTERESTING_FLOATS = [0.0, 1.0, 2.0, sys.float_info.max, float("inf"), float("nan")] @pytest.mark.parametrize( ("start", "end"), [ (a, b) for a in INTERESTING_FLOATS for b in INTERESTING_FLOATS if flt.float_to_lex(a) > flt.float_to_lex(b) ], ) def test_can_shrink_downwards(start, end): assert minimal_from(start, lambda x: not (x < end)) == end @pytest.mark.parametrize( "f", [1, 2, 4, 8, 10, 16, 32, 64, 100, 128, 256, 500, 512, 1000, 1024] ) @pytest.mark.parametrize("mul", [1.1, 1.5, 9.99, 10]) def test_shrinks_downwards_to_integers(f, mul): g = minimal_from(f * mul, lambda x: x >= f) assert g == f def test_shrink_to_integer_upper_bound(): assert minimal_from(1.1, lambda x: 1 < x <= 2) == 2 def test_shrink_up_to_one(): assert minimal_from(0.5, lambda x: 0.5 <= x <= 1.5) == 1 def test_shrink_down_to_half(): assert minimal_from(0.75, lambda x: 0 < x < 1) == 0.5 def test_shrink_fractional_part(): assert minimal_from(2.5, lambda x: divmod(x, 1)[1] == 0.5) == 1.5 def test_does_not_shrink_across_one(): # This is something of an odd special case. Because of our encoding we # prefer all numbers >= 1 to all numbers in 0 < x < 1. For the most part # this is the correct thing to do, but there are some low negative exponent # cases where we get odd behaviour like this. # This test primarily exists to validate that we don't try to subtract one # from the starting point and trigger an internal exception. assert minimal_from(1.1, lambda x: x == 1.1 or 0 < x < 1) == 1.1 def test_reject_out_of_bounds_floats_while_shrinking(): # coverage test for rejecting out of bounds floats while shrinking constraints = {"min_value": 103.0} g = minimal_from(103.1, lambda x: x >= 100, constraints=constraints) assert g == 103.0 @pytest.mark.parametrize("nan", [-math.nan, SIGNALING_NAN, -SIGNALING_NAN]) def test_shrinks_to_canonical_nan(nan): shrunk = minimal_from(nan, math.isnan) assert float_to_int(shrunk) == float_to_int(math.nan) ================================================ FILE: hypothesis-python/tests/conjecture/test_forced.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import pytest import hypothesis.strategies as st from hypothesis import HealthCheck, example, given, settings from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.choice import choice_equal from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.floats import SIGNALING_NAN, SMALLEST_SUBNORMAL from tests.conjecture.common import choice_types_constraints, fresh_data @given(st.data()) @settings( database=None, suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], ) def test_forced_many(data): forced = data.draw(st.integers(0, 100)) min_size = data.draw(st.integers(0, forced)) max_size = data.draw(st.integers(forced, 100)) assert min_size <= forced <= max_size # by construction data = fresh_data() many = cu.many( data, min_size=min_size, average_size=(min_size + max_size) / 2, max_size=max_size, forced=forced, ) for _ in range(forced): assert many.more() assert not many.more() # ensure values written to the buffer do in fact generate the forced value data = ConjectureData.for_choices(data.choices) many = cu.many( data, min_size=min_size, average_size=(min_size + max_size) / 2, max_size=max_size, ) for _ in range(forced): assert many.more() assert not many.more() @example(("boolean", {"p": 1e-19, "forced": True})) # 64 bit p @example(("boolean", {"p": 3e-19, "forced": True})) # 62 bit p @example( ( "integer", { "min_value": -1, "max_value": 1, "shrink_towards": 1, "weights": {-1: 0.2, 0: 0.2, 1: 0.2}, "forced": 0, }, ) ) @example( ( "integer", { "min_value": -1, "max_value": 1, "shrink_towards": -1, "weights": {-1: 0.2, 0: 0.2, 1: 0.2}, "forced": 0, }, ) ) @example( ( "integer", { "min_value": 10, "max_value": 1_000, "shrink_towards": 17, "weights": {20: 0.1}, "forced": 15, }, ) ) @example( ( "integer", { "min_value": -1_000, "max_value": -10, "shrink_towards": -17, "weights": {-20: 0.1}, "forced": -15, }, ) ) @example(("float", {"forced": 0.0})) @example(("float", {"forced": -0.0})) @example(("float", {"forced": 1.0})) @example(("float", {"forced": 1.2345})) @example(("float", {"forced": SMALLEST_SUBNORMAL})) @example(("float", {"forced": -SMALLEST_SUBNORMAL})) @example(("float", {"forced": 100 * SMALLEST_SUBNORMAL})) @example(("float", {"forced": math.nan})) @example(("float", {"forced": -math.nan})) @example(("float", {"forced": SIGNALING_NAN})) @example(("float", {"forced": -SIGNALING_NAN})) @example(("float", {"forced": 1e999})) @example(("float", {"forced": -1e999})) # previously errored on our {pos, neg}_clamper logic not considering nans. @example( ( "float", {"min_value": -1 * math.inf, "max_value": -1 * math.inf, "forced": math.nan}, ) ) @given(choice_types_constraints(use_forced=True)) def test_forced_values(choice_type_and_constraints): (choice_type, constraints) = choice_type_and_constraints constraints = constraints.copy() forced = constraints["forced"] data = fresh_data() assert choice_equal(getattr(data, f"draw_{choice_type}")(**constraints), forced) # now make sure the written buffer reproduces the forced value, even without # specifying forced=. del constraints["forced"] data = ConjectureData.for_choices(data.choices) assert choice_equal(getattr(data, f"draw_{choice_type}")(**constraints), forced) @pytest.mark.parametrize("sign", [1, -1]) @pytest.mark.parametrize( "min_value, max_value", [ (0.0, 0.0), (-0.0, -0.0), (0.0, 100.0), (-100.0, -0.0), (5.0, 10.0), (-10.0, -5.0), ], ) @given(random=st.randoms()) def test_forced_floats_with_nan(random, sign, min_value, max_value): # nans with a sign opposite of both bounds previously gave us trouble # trying to use float clampers that didn't exist when drawing. data = fresh_data(random=random) data.draw_float(min_value=min_value, max_value=max_value, forced=sign * math.nan) @given(st.data()) def test_forced_with_large_magnitude_integers(data): bound_offset = data.draw(st.integers(min_value=0)) # forced_offset = bound_offset + st.integers(min_value=0) may look cleaner, but # has subtly different maximum value semantics as it is twice the range of a # single draw forced_offset = data.draw(st.integers(min_value=bound_offset)) half_range = 2**127 + 1 cd = fresh_data() cd.draw_integer( min_value=half_range + bound_offset, forced=half_range + forced_offset ) cd = fresh_data() cd.draw_integer( max_value=-(half_range + bound_offset), forced=-(half_range + forced_offset) ) ================================================ FILE: hypothesis-python/tests/conjecture/test_inquisitor.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import traceback import pytest from hypothesis import given, settings, strategies as st from tests.common.utils import fails_with def fails_with_output(expected): def _inner(f): def _new(): with pytest.raises(AssertionError) as err: f() if not hasattr(err.value, "__notes__"): traceback.print_exception(err.value) raise Exception( "err.value does not have __notes__, something has gone " "deeply wrong in the internals" ) got = "\n".join(err.value.__notes__).strip() + "\n" assert got == expected.strip() + "\n" return _new return _inner @fails_with_output( """ Falsifying example: test_inquisitor_comments_basic_fail_if_either( # The test always failed when commented parts were varied together. a=False, # or any other generated value b=True, c=[], # or any other generated value d=True, e=False, # or any other generated value ) """ ) @settings(print_blob=False, derandomize=True) @given(st.booleans(), st.booleans(), st.lists(st.none()), st.booleans(), st.booleans()) def test_inquisitor_comments_basic_fail_if_either(a, b, c, d, e): assert not (b and d) @fails_with_output( """ Falsifying example: test_inquisitor_comments_basic_fail_if_not_all( # The test sometimes passed when commented parts were varied together. a='', # or any other generated value b='', # or any other generated value c='', # or any other generated value ) """ ) @settings(print_blob=False, derandomize=True) @given(st.text(), st.text(), st.text()) def test_inquisitor_comments_basic_fail_if_not_all(a, b, c): condition = a and b and c assert condition @fails_with_output( """ Falsifying example: test_inquisitor_no_together_comment_if_single_argument( a='', b='', # or any other generated value ) """ ) @settings(print_blob=False, derandomize=True) @given(st.text(), st.text()) def test_inquisitor_no_together_comment_if_single_argument(a, b): assert a @st.composite def ints_with_forced_draw(draw): data = draw(st.data()) n = draw(st.integers()) data.conjecture_data.draw_boolean(forced=True) return n @fails_with_output( """ Falsifying example: test_inquisitor_doesnt_break_on_varying_forced_nodes( n1=100, n2=0, # or any other generated value ) """ ) @settings(print_blob=False, derandomize=True) @given(st.integers(), ints_with_forced_draw()) def test_inquisitor_doesnt_break_on_varying_forced_nodes(n1, n2): assert n1 < 100 @fails_with(ZeroDivisionError) @settings(database=None) @given(start_date=st.datetimes(), data=st.data()) def test_issue_3755_regression(start_date, data): data.draw(st.datetimes(min_value=start_date)) raise ZeroDivisionError # Tests for sub-argument explanations class MyClass: def __init__(self, x, y): self.x = x self.y = y @fails_with_output( """ Falsifying example: test_inquisitor_builds_subargs( obj=MyClass( 0, # or any other generated value True, ), ) """ ) @settings(print_blob=False, derandomize=True) @given(st.builds(MyClass, st.integers(), st.booleans())) def test_inquisitor_builds_subargs(obj): assert not obj.y @fails_with_output( """ Falsifying example: test_inquisitor_builds_kwargs_subargs( obj=MyClass( x=0, # or any other generated value y=True, ), ) """ ) @settings(print_blob=False, derandomize=True) @given(st.builds(MyClass, x=st.integers(), y=st.booleans())) def test_inquisitor_builds_kwargs_subargs(obj): assert not obj.y @fails_with_output( """ Falsifying example: test_inquisitor_tuple_subargs( t=( 0, # or any other generated value True, ), ) """ ) @settings(print_blob=False, derandomize=True) @given(st.tuples(st.integers(), st.booleans())) def test_inquisitor_tuple_subargs(t): assert not t[1] @fails_with_output( """ Falsifying example: test_inquisitor_fixeddict_subargs( d={ 'x': 0, # or any other generated value 'y': True, }, ) """ ) @settings(print_blob=False, derandomize=True) @given(st.fixed_dictionaries({"x": st.integers(), "y": st.booleans()})) def test_inquisitor_fixeddict_subargs(d): assert not d["y"] @fails_with_output( """ Falsifying example: test_inquisitor_tuple_multiple_varying( t=( 0, # or any other generated value '', # or any other generated value True, ), ) """ ) @settings(print_blob=False, derandomize=True) @given(st.tuples(st.integers(), st.text(), st.booleans())) def test_inquisitor_tuple_multiple_varying(t): # Multiple sub-arguments can vary, but the "together" comment only applies # to top-level test arguments, not to sub-arguments within composites. assert not t[2] @fails_with_output( """ Falsifying example: test_inquisitor_skip_subset_slices( obj=MyClass( (0, False), # or any other generated value y=False, ), ) """ ) @settings(print_blob=False, derandomize=True) @given(st.builds(MyClass, st.tuples(st.integers(), st.booleans()), y=st.booleans())) def test_inquisitor_skip_subset_slices(obj): # The tuple can vary freely, but the booleans inside it shouldn't # get individual comments since they're part of a larger varying slice. assert obj.y # Test for duplicate param names at different nesting levels @fails_with_output( """ Falsifying example: test_inquisitor_duplicate_param_names( kw=0, # or any other generated value b={ 'kw': '', # or any other generated value 'c': True, }, ) """ ) @settings(print_blob=False, derandomize=True) @given(kw=st.integers(), b=st.fixed_dictionaries({"kw": st.text(), "c": st.booleans()})) def test_inquisitor_duplicate_param_names(kw, b): # Both "kw" (top-level) and "b['kw']" can vary - they get separate comments # even though they have the same name at different nesting levels. # b['c'] is the critical value that determines the failure. assert not b["c"] # Test for multi-level nesting: bare, nested, and double-nested class Outer: def __init__(self, inner, value): self.inner = inner self.value = value class Inner: def __init__(self, x): self.x = x @fails_with_output( """ Falsifying example: test_inquisitor_multi_level_nesting( bare=0, # or any other generated value outer=Outer( inner=Inner(x=0), # or any other generated value value=True, ), ) """ ) @settings(print_blob=False, derandomize=True) @given( bare=st.integers(), outer=st.builds( Outer, inner=st.builds(Inner, x=st.integers()), value=st.booleans() ), ) def test_inquisitor_multi_level_nesting(bare, outer): # Comments on: bare (top-level) and outer.inner (nested). The inner.x slice # is the same as inner's slice since Inner has only one argument, so the # comment appears only once on inner. outer.value doesn't get a comment # because it's the critical value that determines the failure. assert not outer.value ================================================ FILE: hypothesis-python/tests/conjecture/test_intlist.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import assume, given, strategies as st from hypothesis.internal.conjecture.junkdrawer import IntList non_neg_lists = st.lists(st.integers(min_value=0, max_value=2**63 - 1)) @given(non_neg_lists) def test_intlist_is_equal_to_itself(ls): assert IntList(ls) == IntList(ls) @given(non_neg_lists, non_neg_lists) def test_distinct_int_lists_are_not_equal(x, y): assume(x != y) assert IntList(x) != IntList(y) def test_basic_equality(): x = IntList([1, 2, 3]) assert x == x t = x != x assert not t assert x != "foo" s = x == "foo" assert not s def test_error_on_invalid_value(): with pytest.raises(ValueError): IntList([-1]) def test_extend_by_too_large(): x = IntList() ls = [1, 10**6] x.extend(ls) assert list(x) == ls ================================================ FILE: hypothesis-python/tests/conjecture/test_junkdrawer.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import copy import inspect import sys import warnings import pytest from hypothesis import example, given, strategies as st from hypothesis.errors import HypothesisWarning from hypothesis.internal.conjecture import junkdrawer from hypothesis.internal.conjecture.junkdrawer import ( IntList, LazySequenceCopy, binary_search, endswith, ensure_free_stackframes, replace_all, stack_depth_of_caller, startswith, ) from hypothesis.internal.floats import clamp, float_to_int, sign_aware_lte from tests.common.utils import restore_recursion_limit def test_out_of_range(): x = LazySequenceCopy([1, 2, 3]) with pytest.raises(IndexError): x[3] with pytest.raises(IndexError): x[-4] def test_pass_through(): x = LazySequenceCopy([1, 2, 3]) assert x[0] == 1 assert x[1] == 2 assert x[2] == 3 def test_can_assign_without_changing_underlying(): underlying = [1, 2, 3] x = LazySequenceCopy(underlying) x[1] = 10 assert x[1] == 10 assert underlying[1] == 2 def test_pop(): x = LazySequenceCopy([2, 3]) assert x.pop() == 3 assert x.pop() == 2 with pytest.raises(IndexError): x.pop() @st.composite def clamp_inputs(draw): lower = draw(st.floats(allow_nan=False)) value = draw(st.floats(allow_nan=False)) upper = draw(st.floats(min_value=lower, allow_nan=False)) return (lower, value, upper) @example((1, 5, 10)) @example((1, 10, 5)) @example((5, 10, 5)) @example((5, 1, 10)) @example((-5, 0.0, -0.0)) @example((0.0, -0.0, 5)) @example((-0.0, 0.0, 0.0)) @example((-0.0, -0.0, 0.0)) @given(clamp_inputs()) def test_clamp(input): lower, value, upper = input clamped = clamp(lower, value, upper) assert sign_aware_lte(lower, clamped) assert sign_aware_lte(clamped, upper) if sign_aware_lte(lower, value) and sign_aware_lte(value, upper): assert float_to_int(value) == float_to_int(clamped) if lower > value: assert float_to_int(clamped) == float_to_int(lower) if value > upper: assert float_to_int(clamped) == float_to_int(upper) # this would be more robust as a stateful test, where each rule is a list operation # on (1) the canonical python list and (2) its LazySequenceCopy. We would assert # that the return values and lists match after each rule, and the original list # is unmodified. @pytest.mark.parametrize("should_mask", [True, False]) @given(lst=st.lists(st.integers(), min_size=1), data=st.data()) def test_pop_sequence_copy(lst, data, should_mask): original = copy.copy(lst) pop_i = data.draw(st.integers(0, len(lst) - 1)) if should_mask: mask_i = data.draw(st.integers(0, len(lst) - 1)) mask_value = data.draw(st.integers()) def pop(l): if should_mask: l[mask_i] = mask_value return l.pop(pop_i) expected = copy.copy(lst) l = LazySequenceCopy(lst) assert pop(expected) == pop(l) assert list(l) == expected # modifications to the LazySequenceCopy should not modify the original list assert original == lst def test_assignment(): y = [1, 2, 3] x = LazySequenceCopy(y) x[-1] = 5 assert list(x) == [1, 2, 5] x[-1] = 7 assert list(x) == [1, 2, 7] def test_replacement(): result = replace_all([1, 1, 1, 1], [(1, 3, [3, 4])]) assert result == [1, 3, 4, 1] def test_int_list_cannot_contain_negative(): with pytest.raises(ValueError): IntList([-1]) def test_int_list_can_contain_arbitrary_size(): n = 2**65 assert list(IntList([n])) == [n] def test_int_list_of_length(): assert list(IntList.of_length(10)) == [0] * 10 def test_int_list_equality(): ls = [1, 2, 3] x = IntList(ls) y = IntList(ls) assert ls != x assert x != ls assert not (x == ls) # noqa assert x == x assert x == y def test_int_list_extend(): x = IntList.of_length(3) n = 2**64 - 1 x.extend([n]) assert list(x) == [0, 0, 0, n] def test_int_list_slice(): x = IntList([1, 2]) assert list(x[:1]) == [1] assert list(x[0:2]) == [1, 2] assert list(x[1:]) == [2] def test_int_list_del(): x = IntList([1, 2]) del x[0] assert x == IntList([2]) def test_int_list_insert(): x = IntList([1, 3]) x.insert(1, 2) assert x == IntList([1, 2, 3]) @pytest.mark.parametrize("n", [0, 1, 30, 70]) def test_binary_search(n): i = binary_search(0, 100, lambda i: i <= n) assert i == n def recur(i): assert len(inspect.stack(0)) == stack_depth_of_caller() if i >= 1: recur(i - 1) def test_stack_size_detection(): recur(100) @given(st.binary(), st.binary()) def test_startswith(b1, b2): assert b1.startswith(b2) == startswith(b1, b2) @given(st.binary(), st.binary()) def test_endswith(b1, b2): assert b1.endswith(b2) == endswith(b1, b2) def test_stackframes_warns_when_recursion_limit_is_changed(): match = ( "The recursion limit will not be reset, since it was changed during " "test execution." ) with ( restore_recursion_limit(), pytest.warns(HypothesisWarning, match=match) as warnings, ensure_free_stackframes(), ): sys.setrecursionlimit(100) # we only got the warning once assert len(warnings) == 1 def test_stackframes_cleans_up_on_werror(): limiter = junkdrawer._stackframe_limiter with restore_recursion_limit(), warnings.catch_warnings(): warnings.simplefilter("error") assert limiter._active_contexts == 0 # the code for this cleanup case only triggers when the warning is raised # on __enter__. set that up by entering one context, changing the limit, # then entering another. with pytest.raises(HypothesisWarning), ensure_free_stackframes(): assert limiter._active_contexts == 1 sys.setrecursionlimit(101) with ensure_free_stackframes(): assert limiter._active_contexts == 2 sys.setrecursionlimit(102) assert limiter._active_contexts == 1 assert limiter._active_contexts == 0 ================================================ FILE: hypothesis-python/tests/conjecture/test_local_constants.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys from types import SimpleNamespace import pytest from hypothesis import given, settings, strategies as st from hypothesis.internal.conjecture import providers from hypothesis.internal.conjecture.choice import choice_equal from hypothesis.internal.conjecture.providers import CONSTANTS_CACHE from hypothesis.internal.constants_ast import Constants from tests.common.debug import find_any from tests.common.utils import Why, xfail_on_crosshair # I tried using @given(st.integers()) here, but I think there is a bad interaction # with CONSTANTS_CACHE when testing it inside of a hypothesis test. @pytest.mark.parametrize("value", [2**20 - 50, 2**10 - 10, 129387123, -19827321, 0]) def test_can_draw_local_constants_integers(monkeypatch, value): # _get_local_constants normally invalidates this cache for us, but we're # monkeypatching it. CONSTANTS_CACHE.cache.clear() monkeypatch.setattr( providers, "_get_local_constants", lambda: Constants(integers={value}) ) find_any(st.integers(), lambda v: choice_equal(v, value)) @xfail_on_crosshair(Why.undiscovered) # I think float_to_int is difficult for crosshair @pytest.mark.parametrize("value", [1.2938, -1823.0239, 1e999, math.nan]) def test_can_draw_local_constants_floats(monkeypatch, value): CONSTANTS_CACHE.cache.clear() monkeypatch.setattr( providers, "_get_local_constants", lambda: Constants(floats={value}) ) find_any(st.floats(), lambda v: choice_equal(v, value)) @pytest.mark.parametrize("value", [b"abdefgh", b"a" * 50]) def test_can_draw_local_constants_bytes(monkeypatch, value): CONSTANTS_CACHE.cache.clear() monkeypatch.setattr( providers, "_get_local_constants", lambda: Constants(bytes={value}) ) find_any(st.binary(), lambda v: choice_equal(v, value)) @pytest.mark.parametrize("value", ["abdefgh", "a" * 50]) def test_can_draw_local_constants_string(monkeypatch, value): CONSTANTS_CACHE.cache.clear() monkeypatch.setattr( providers, "_get_local_constants", lambda: Constants(strings={value}) ) # we have a bunch of strings in GLOBAL_CONSTANTS, so it might take a while # to generate our local constant. find_any( st.text(), lambda v: choice_equal(v, value), settings=settings(max_examples=5_000), ) def test_actual_collection(monkeypatch, tmp_path): # covering test for doing some real work collecting constants. We'll fake # hypothesis as being the "local" module, just to get some real constant # collection going. # reset cache checks monkeypatch.setattr(providers, "_sys_modules_len", None) monkeypatch.setattr(providers, "_seen_modules", set()) monkeypatch.setattr(providers, "is_local_module_file", lambda f: "hypothesis" in f) # Without monkeypatching this, this test only covers all the right lines the # first time it's executed, since it falls back to the cache the second time. # Reset to a guaranteed-empty storage directory to ensure consistent coverage. monkeypatch.setattr( "hypothesis.internal.constants_ast.storage_directory", lambda *names, **kwargs: tmp_path.joinpath(*names), ) @given(st.integers()) @settings(max_examples=100) def f(n): pass f() def test_unhashable_sys_modules_entry(monkeypatch): # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/4660 # Some packages (e.g. cog) place unhashable objects like SimpleNamespace # in sys.modules. This should not crash _get_local_constants. monkeypatch.setattr(providers, "_sys_modules_len", None) monkeypatch.setattr(providers, "_seen_modules", set()) monkeypatch.setitem(sys.modules, "_unhashable_test_mod", SimpleNamespace()) providers._get_local_constants() ================================================ FILE: hypothesis-python/tests/conjecture/test_minimizer.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import Counter import pytest from hypothesis.internal.conjecture.shrinking import ( Bytes, Collection, Integer, Ordering, String, ) from hypothesis.internal.intervalsets import IntervalSet def test_shrink_to_zero(): assert Integer.shrink(2**16, lambda n: True) == 0 def test_shrink_to_smallest(): assert Integer.shrink(2**16, lambda n: n > 10) == 11 def test_can_sort_bytes_by_reordering(): start = bytes([5, 4, 3, 2, 1, 0]) finish = Ordering.shrink(start, lambda x: set(x) == set(start)) assert bytes(finish) == bytes([0, 1, 2, 3, 4, 5]) def test_can_sort_bytes_by_reordering_partially(): start = bytes([5, 4, 3, 2, 1, 0]) finish = Ordering.shrink(start, lambda x: set(x) == set(start) and x[0] > x[-1]) assert bytes(finish) == bytes([1, 2, 3, 4, 5, 0]) def test_can_sort_bytes_by_reordering_partially2(): start = bytes([5, 4, 3, 2, 1, 0]) finish = Ordering.shrink( start, lambda x: Counter(x) == Counter(start) and x[0] > x[2], full=True, ) assert bytes(finish) == bytes([1, 2, 0, 3, 4, 5]) def test_can_sort_bytes_by_reordering_partially_not_cross_stationary_element(): start = bytes([5, 3, 0, 2, 1, 4]) finish = Ordering.shrink(start, lambda x: set(x) == set(start) and x[3] == 2) assert bytes(finish) == bytes([0, 1, 3, 2, 4, 5]) @pytest.mark.parametrize( "initial, predicate, intervals, expected", [ ("f" * 10, lambda s: True, IntervalSet.from_string("abcdefg"), ""), ("f" * 10, lambda s: len(s) >= 3, IntervalSet.from_string("abcdefg"), "aaa"), ( "f" * 10, lambda s: len(s) >= 3 and "a" not in s, IntervalSet.from_string("abcdefg"), "bbb", ), ], ) def test_shrink_strings(initial, predicate, intervals, expected): assert String.shrink( initial, predicate, intervals=intervals, min_size=len(expected) ) == tuple(expected) @pytest.mark.parametrize( "initial, predicate, expected", [ (b"\x18\x12", lambda v: len(v) == 2, b"\x00\x00"), (b"\x18\x12", lambda v: True, b""), (b"\x01\x10", lambda v: len(v) > 0 and v[0] == 1, b"\x01"), (b"\x01\x10\x01\x92", lambda v: sum(v) >= 9, b"\x09"), ], ) def test_shrink_bytes(initial, predicate, expected): assert bytes(Bytes.shrink(initial, predicate, min_size=len(expected))) == expected def test_collection_left_is_better(): shrinker = Collection( [1, 2, 3], lambda v: True, ElementShrinker=Integer, min_size=3 ) assert not shrinker.left_is_better([1, 2, 3], [1, 2, 3]) ================================================ FILE: hypothesis-python/tests/conjecture/test_mutations.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import strategies as st from tests.common.debug import find_any tree = st.deferred(lambda: st.tuples(st.integers(), tree, tree)) | st.just(None) def test_can_find_duplicated_subtree(): # look for an example of the form # # ┌─────┐ # ┌──────┤ a ├──────┐ # │ └─────┘ │ # ┌──┴──┐ ┌──┴──┐ # │ b │ │ a │ # └──┬──┘ └──┬──┘ # ┌────┴────┐ ┌────┴────┐ # ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ # │ c │ │ d │ │ b │ │ ... │ # └─────┘ └─────┘ └──┬──┘ └─────┘ # ┌────┴────┐ # ┌──┴──┐ ┌──┴──┐ # │ c │ │ d │ # └─────┘ └─────┘ # # If we just checked that (b, c, d) was duplicated somewhere, this could have # happened as a result of normal mutation. Checking for the a parent node as # well is unlikely to have been generated without tree mutation, however. find_any( tree, ( lambda v: v is not None and v[1] is not None and v[2] is not None and v[0] == v[2][0] and v[1] == v[2][1] ), ) ================================================ FILE: hypothesis-python/tests/conjecture/test_optimiser.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from random import Random import pytest from hypothesis import HealthCheck, assume, example, given, settings from hypothesis.internal.conjecture.choice import ChoiceNode from hypothesis.internal.conjecture.data import Status from hypothesis.internal.conjecture.datatree import compute_max_children from hypothesis.internal.conjecture.engine import ConjectureRunner, RunIsComplete from hypothesis.internal.intervalsets import IntervalSet from tests.conjecture.common import ( buffer_size_limit, integer_constr, nodes, ) runner_settings = settings( max_examples=100, database=None, suppress_health_check=list(HealthCheck) ) def test_optimises_to_maximum(): def test(data): data.target_observations["m"] = data.draw_integer(0, 2**8 - 1) runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) runner.cached_test_function((0,)) try: runner.optimise_targets() except RunIsComplete: pass assert runner.best_observed_targets["m"] == 255 def test_optimises_multiple_targets(): def test(data): n = data.draw_integer(0, 2**8 - 1) m = data.draw_integer(0, 2**8 - 1) if n + m > 256: data.mark_invalid() data.target_observations["m"] = m data.target_observations["n"] = n data.target_observations["m + n"] = m + n runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) runner.cached_test_function((200, 0)) runner.cached_test_function((0, 200)) try: runner.optimise_targets() except RunIsComplete: pass assert runner.best_observed_targets["m"] == 255 assert runner.best_observed_targets["n"] == 255 assert runner.best_observed_targets["m + n"] == 256 def test_optimises_when_last_element_is_empty(): def test(data): data.target_observations["n"] = data.draw_integer(0, 2**8 - 1) data.start_span(label=1) data.stop_span() runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) runner.cached_test_function((250,)) try: runner.optimise_targets() except RunIsComplete: pass assert runner.best_observed_targets["n"] == 255 def test_can_optimise_last_with_following_empty(): def test(data): for _ in range(100): data.draw_integer(0, 3) data.target_observations[""] = data.draw_integer(0, 2**8 - 1) data.start_span(1) data.stop_span() runner = ConjectureRunner( test, settings=settings(runner_settings, max_examples=100), random=Random(0) ) runner.cached_test_function((0,) * 101) with pytest.raises(RunIsComplete): runner.optimise_targets() assert runner.best_observed_targets[""] == 255 @pytest.mark.parametrize("lower, upper", [(0, 1000), (13, 100), (1000, 2**16 - 1)]) @pytest.mark.parametrize("score_up", [False, True]) def test_can_find_endpoints_of_a_range(lower, upper, score_up): def test(data): n = data.draw_integer(0, 2**16 - 1) if n < lower or n > upper: data.mark_invalid() if not score_up: n = -n data.target_observations["n"] = n runner = ConjectureRunner( test, settings=settings(runner_settings, max_examples=1000), random=Random(0) ) runner.cached_test_function(((lower + upper) // 2,)) try: runner.optimise_targets() except RunIsComplete: pass if score_up: assert runner.best_observed_targets["n"] == upper else: assert runner.best_observed_targets["n"] == -lower def test_targeting_can_drive_length_very_high(): def test(data): count = 0 while data.draw_boolean(0.25): count += 1 data.target_observations[""] = min(count, 100) runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) # extend here to ensure we get a valid (non-overrun) test case. The # outcome of the test case doesn't really matter as long as we have # something for the runner to optimize. runner.cached_test_function([], extend=50) try: runner.optimise_targets() except RunIsComplete: pass assert runner.best_observed_targets[""] == 100 def test_optimiser_when_test_grows_buffer_to_invalid(): def test(data): m = data.draw_integer(0, 2**8 - 1) data.target_observations["m"] = m if m > 100: data.draw_integer(0, 2**16 - 1) data.mark_invalid() runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) runner.cached_test_function((0,) * 10) try: runner.optimise_targets() except RunIsComplete: pass assert runner.best_observed_targets["m"] == 100 def test_can_patch_up_examples(): def test(data): data.start_span(42) m = data.draw_integer(0, 2**6 - 1) data.target_observations["m"] = m for _ in range(m): data.draw_boolean() data.stop_span() for i in range(4): if i != data.draw_integer(0, 2**8 - 1): data.mark_invalid() runner = ConjectureRunner( test, settings=settings(runner_settings, max_examples=1000), random=Random(0) ) d = runner.cached_test_function((0, 0, 1, 2, 3, 4)) assert d.status == Status.VALID try: runner.optimise_targets() except RunIsComplete: pass assert runner.best_observed_targets["m"] == 63 def test_optimiser_when_test_grows_buffer_to_overflow(): def test(data): m = data.draw_integer(0, 2**8 - 1) data.target_observations["m"] = m if m > 100: data.draw_integer(0, 2**64 - 1) data.mark_invalid() runner = ConjectureRunner(test, settings=runner_settings, random=Random(0)) with buffer_size_limit(2): runner.cached_test_function((0,) * 10) try: runner.optimise_targets() except RunIsComplete: pass assert runner.best_observed_targets["m"] == 100 @given(nodes()) @example( ChoiceNode( type="bytes", value=b"\xb1", constraints={"min_size": 1, "max_size": 1}, was_forced=False, ) ) @example( ChoiceNode( type="string", value="aaaa", constraints={ "min_size": 0, "max_size": 10, "intervals": IntervalSet.from_string("abcd"), }, was_forced=False, ) ) @example( ChoiceNode( type="integer", value=1, constraints=integer_constr(0, 200), was_forced=False ) ) def test_optimising_all_nodes(node): assume(compute_max_children(node.type, node.constraints) > 50) size_function = { "integer": lambda n: n, "float": lambda f: f if math.isfinite(f) else 0, "string": lambda s: len(s), "bytes": lambda b: len(b), "boolean": lambda b: int(b), } def test(data): v = getattr(data, f"draw_{node.type}")(**node.constraints) data.target_observations["v"] = size_function[node.type](v) runner = ConjectureRunner( test, settings=settings(runner_settings, max_examples=50), random=Random(0) ) runner.cached_test_function([node.value]) try: runner.optimise_targets() except RunIsComplete: pass ================================================ FILE: hypothesis-python/tests/conjecture/test_order_shrinking.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import example, given, strategies as st from hypothesis.internal.conjecture.shrinking import Ordering @example([0, 1, 1, 1, 1, 1, 1, 0]) @example([0, 0]) @example([0, 1, -1]) @given(st.lists(st.integers())) def test_shrinks_down_to_sorted_the_slow_way(ls): # We normally would short-circuit and find that we can sort this # automatically, but here we test that a single run_step could put the # list in sorted order anyway if it had to, and that that is just an # optimisation. shrinker = Ordering(ls, lambda ls: True, full=False) shrinker.run_step() assert list(shrinker.current) == sorted(ls) def test_can_partially_sort_a_list(): finish = Ordering.shrink([5, 4, 3, 2, 1, 0], lambda x: x[0] > x[-1]) assert finish == (1, 2, 3, 4, 5, 0) def test_can_partially_sort_a_list_2(): finish = Ordering.shrink([5, 4, 3, 2, 1, 0], lambda x: x[0] > x[2], full=True) assert finish <= (1, 2, 0, 3, 4, 5) def test_adaptively_shrinks_around_hole(): initial = list(range(1000, 0, -1)) initial[500] = 2000 intended_result = sorted(initial) intended_result.insert(500, intended_result.pop()) shrinker = Ordering(initial, lambda ls: ls[500] == 2000, full=True) shrinker.run() assert shrinker.current[500] == 2000 assert list(shrinker.current) == intended_result assert shrinker.calls <= 60 ================================================ FILE: hypothesis-python/tests/conjecture/test_pareto.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import contextlib import itertools from random import Random import pytest from hypothesis import HealthCheck, Phase, settings, strategies as st from hypothesis.database import ( InMemoryExampleDatabase, choices_from_bytes, choices_to_bytes, ) from hypothesis.errors import StopTest from hypothesis.internal.conjecture.data import ConjectureData, Status from hypothesis.internal.conjecture.engine import ConjectureRunner, RunIsComplete from hypothesis.internal.conjecture.pareto import ParetoFront from tests.conjecture.common import interesting_origin def test_pareto_front_contains_different_interesting_reasons(): def test(data): data.target_observations[""] = 1 n = data.draw_integer(0, 2**4 - 1) data.mark_interesting(interesting_origin(n)) runner = ConjectureRunner( test, settings=settings( max_examples=5000, database=InMemoryExampleDatabase(), suppress_health_check=list(HealthCheck), ), database_key=b"stuff", random=Random(0), ) runner.run() assert len(runner.pareto_front) == 2**4 def test_pareto_front_omits_invalid_examples(): def test(data): x = data.draw_integer(0, 2**4 - 1) if x % 2: data.target_observations[""] = 1 data.mark_invalid() runner = ConjectureRunner( test, settings=settings( max_examples=5000, database=InMemoryExampleDatabase(), suppress_health_check=list(HealthCheck), ), database_key=b"stuff", random=Random(0), ) runner.run() assert len(runner.pareto_front) == 0 def test_database_contains_only_pareto_front(): def test(data): data.target_observations["1"] = data.draw(st.integers(0, 2**4)) data.draw(st.integers(0, 2**64)) data.target_observations["2"] = data.draw(st.integers(0, 2**8)) assert len(set(db.fetch(b"stuff.pareto"))) == len(runner.pareto_front) db = InMemoryExampleDatabase() runner = ConjectureRunner( test, settings=settings( max_examples=500, database=db, suppress_health_check=list(HealthCheck) ), database_key=b"stuff", random=Random(0), ) runner.run() assert len(runner.pareto_front) <= 500 for v in runner.pareto_front: assert v.status >= Status.VALID values = set(db.fetch(b"stuff.pareto")) assert len(values) == len(runner.pareto_front), { choices_to_bytes(data.choices) for data in runner.pareto_front }.symmetric_difference(values) for data in runner.pareto_front: assert choices_to_bytes(data.choices) in values assert data in runner.pareto_front for b in values: choices = choices_from_bytes(b) assert runner.cached_test_function(choices) in runner.pareto_front def test_clears_defunct_pareto_front(): def test(data): data.target_observations[""] = 1 data.draw_integer(0, 2**8 - 1) data.draw_integer(0, 2**8 - 1) db = InMemoryExampleDatabase() runner = ConjectureRunner( test, settings=settings( max_examples=10000, database=db, suppress_health_check=list(HealthCheck), phases=[Phase.reuse], ), database_key=b"stuff", random=Random(0), ) for i in range(256): db.save(runner.pareto_key, choices_to_bytes((i, 0))) runner.run() assert len(list(db.fetch(runner.pareto_key))) == 1 def test_down_samples_the_pareto_front(): def test(data): data.draw_integer(0, 2**8 - 1) data.draw_integer(0, 2**8 - 1) db = InMemoryExampleDatabase() runner = ConjectureRunner( test, settings=settings( max_examples=1000, database=db, suppress_health_check=list(HealthCheck), phases=[Phase.reuse], ), database_key=b"stuff", random=Random(0), ) for n1, n2 in itertools.product(range(256), range(256)): db.save(runner.pareto_key, choices_to_bytes((n1, n2))) with pytest.raises(RunIsComplete): runner.reuse_existing_examples() assert runner.valid_examples == 1000 def test_stops_loading_pareto_front_if_interesting(): def test(data): data.draw_integer() data.draw_integer() data.mark_interesting(interesting_origin()) db = InMemoryExampleDatabase() runner = ConjectureRunner( test, settings=settings( max_examples=1000, database=db, suppress_health_check=list(HealthCheck), phases=[Phase.reuse], ), database_key=b"stuff", random=Random(0), ) for n1, n2 in itertools.product(range(256), range(256)): db.save(runner.pareto_key, choices_to_bytes((n1, n2))) runner.reuse_existing_examples() assert runner.call_count == 1 def test_uses_tags_in_calculating_pareto_front(): def test(data): data.target_observations[""] = 1 if data.draw_boolean(): data.start_span(11) data.draw_integer(0, 2**8 - 1) data.stop_span() runner = ConjectureRunner( test, settings=settings(max_examples=10, database=InMemoryExampleDatabase()), database_key=b"stuff", random=Random(0), ) runner.run() assert len(runner.pareto_front) == 2 def test_optimises_the_pareto_front(): def test(data): count = 0 while data.draw_integer(0, 2**8 - 1): count += 1 data.target_observations[""] = min(count, 5) runner = ConjectureRunner( test, settings=settings(max_examples=10000, database=InMemoryExampleDatabase()), database_key=b"stuff", ) runner.cached_test_function([255] * 20 + [0]) runner.pareto_optimise() assert len(runner.pareto_front) == 6 for i, data in enumerate(runner.pareto_front): assert data.choices == (1,) * i + (0,) def test_does_not_optimise_the_pareto_front_if_interesting(): def test(data): n = data.draw_integer(0, 2**8 - 1) data.target_observations[""] = n if n == 255: data.mark_interesting(interesting_origin()) runner = ConjectureRunner( test, settings=settings(max_examples=10000, database=InMemoryExampleDatabase()), database_key=b"stuff", ) runner.cached_test_function([0]) runner.pareto_optimise = None runner.optimise_targets() assert runner.interesting_examples def test_stops_optimising_once_interesting(): hi = 2**16 - 1 def test(data): n = data.draw_integer(0, 2**16 - 1) data.target_observations[""] = n if n < hi: data.mark_interesting(interesting_origin()) runner = ConjectureRunner( test, settings=settings(max_examples=10000, database=InMemoryExampleDatabase()), database_key=b"stuff", ) data = runner.cached_test_function([hi]) assert data.status == Status.VALID runner.pareto_optimise() assert runner.call_count <= 20 assert runner.interesting_examples def test_pareto_contains(): front = ParetoFront(random=Random()) assert "not a data" not in front data = ConjectureData.for_choices([]) with contextlib.suppress(StopTest): data.mark_overrun() # check a data which turns into an Overrun when .as_result is called assert data not in front ================================================ FILE: hypothesis-python/tests/conjecture/test_provider.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import dataclasses import itertools import math import sys import time import warnings from contextlib import contextmanager, nullcontext from random import Random from threading import RLock import pytest from hypothesis import ( HealthCheck, Verbosity, assume, errors, given, settings, strategies as st, ) from hypothesis.control import current_build_context from hypothesis.database import InMemoryExampleDatabase from hypothesis.errors import ( BackendCannotProceed, Flaky, FlakyBackendFailure, HypothesisException, HypothesisWarning, InvalidArgument, Unsatisfiable, ) from hypothesis.internal.compat import WINDOWS, int_to_bytes from hypothesis.internal.conjecture.data import ConjectureData, PrimitiveProvider from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.conjecture.provider_conformance import run_conformance_test from hypothesis.internal.conjecture.providers import ( AVAILABLE_PROVIDERS, COLLECTION_DEFAULT_MAX_SIZE, HypothesisProvider, with_register_backend, ) from hypothesis.internal.floats import SIGNALING_NAN, clamp from hypothesis.internal.intervalsets import IntervalSet from hypothesis.internal.observability import Observation, _callbacks from tests.common.debug import minimal from tests.common.utils import ( capture_observations, capture_out, ) from tests.conjecture.common import nodes class PrngProvider(PrimitiveProvider): # A test-only implementation of the PrimitiveProvider interface, which uses # a very simple PRNG to choose each value. Dumb but efficient, and entirely # independent of our real backend def __init__(self, conjecturedata: "ConjectureData | None", /) -> None: super().__init__(conjecturedata) self.prng = Random(0) def draw_boolean( self, p: float = 0.5, ) -> bool: return self.prng.random() < p def draw_integer( self, min_value: int | None = None, max_value: int | None = None, *, weights: dict[int, float] | None = None, shrink_towards: int = 0, ) -> int: # shrink_towards is fully ignored here. It would be nice to implement # weights, but it's tricky to fully conform it to our # provider_conformance test. assert isinstance(shrink_towards, int) assert weights is None or isinstance(weights, dict) if min_value is None and max_value is None: min_value = -(2**127) max_value = 2**127 - 1 elif min_value is None: min_value = max_value - 2**64 elif max_value is None: max_value = min_value + 2**64 return self.prng.randint(min_value, max_value) def draw_float( self, *, min_value: float = -math.inf, max_value: float = math.inf, allow_nan: bool = True, smallest_nonzero_magnitude: float, ) -> float: if allow_nan and self.prng.random() < 1 / 32: nans = [math.nan, -math.nan, SIGNALING_NAN, -SIGNALING_NAN] return self.prng.choice(nans) # small chance of inf values, if they are in bounds if min_value <= math.inf <= max_value and self.prng.random() < 1 / 32: return math.inf if min_value <= -math.inf <= max_value and self.prng.random() < 1 / 32: return -math.inf # get rid of infs, they cause nans if we pass them to prng.uniform min_value_bound = min_value max_value_bound = max_value if min_value in [-math.inf, math.inf]: min_value_bound = math.copysign(1, min_value) * sys.float_info.max # being too close to the bounds causes prng.uniform to only return # inf. min_value_bound /= 2 if max_value in [-math.inf, math.inf]: max_value_bound = math.copysign(1, max_value) * sys.float_info.max max_value_bound /= 2 value = self.prng.uniform(min_value_bound, max_value_bound) # random.uniform can in fact provide out of bounds values, e.g. # random.uniform(-8.98846567431158e+307, 8.98846567431158e+307) # can produce math.inf, which is strictly greater than 8.98846567431158e+307 value = clamp(min_value, value, max_value) if value and abs(value) < smallest_nonzero_magnitude: return ( smallest_nonzero_magnitude if min_value <= smallest_nonzero_magnitude <= max_value else -smallest_nonzero_magnitude ) return value def draw_string( self, intervals: IntervalSet, *, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, ) -> str: size = self.prng.randint( min_size, max(min_size, min(100 if max_size is None else max_size, 100)) ) if len(intervals) == 0: return "" return "".join(map(chr, self.prng.choices(intervals, k=size))) def draw_bytes( self, min_size: int = 0, max_size: int = COLLECTION_DEFAULT_MAX_SIZE, ) -> bytes: # cap max size for performance max_size = 100 if max_size is None else min(max_size, 100) size = self.prng.randint(min_size, max_size) return self.prng.randbytes(size) _temp_register_backend_lock = RLock() # same as with_register_backend, but adds a lock for our threading tests. @contextmanager def temp_register_backend(name, provider_cls): with _temp_register_backend_lock, with_register_backend(name, provider_cls): yield @pytest.mark.parametrize( "strategy", [ st.booleans(), st.integers(0, 3), st.floats(0, 1), st.text(max_size=3), st.binary(max_size=3), ], ids=repr, ) def test_find_with_backend_then_convert_to_buffer_shrink_and_replay(strategy): db = InMemoryExampleDatabase() assert not db.data with temp_register_backend("prng", PrngProvider): @settings(database=db, backend="prng") @given(strategy) def test(value): if isinstance(value, float): assert value >= 0.5 else: assert value with pytest.raises(AssertionError): test() assert db.data buffers = {x for x in db.data[next(iter(db.data))] if x} assert buffers, db.data def test_backend_can_shrink_integers(): with temp_register_backend("prng", PrngProvider): n = minimal( st.integers(), lambda n: n >= 123456, settings=settings(backend="prng", database=None), ) assert n == 123456 def test_backend_can_shrink_bytes(): with temp_register_backend("prng", PrngProvider): b = minimal( # this test doubles as coverage for popping draw_bytes ir nodes, # and that path is only taken with fixed size for the moment. can # be removed when we support variable length binary at the ir level. st.binary(min_size=2, max_size=2), lambda b: len(b) >= 2 and b[1] >= 10, settings=settings(backend="prng", database=None), ) assert b == int_to_bytes(10, size=2) def test_backend_can_shrink_strings(): with temp_register_backend("prng", PrngProvider): s = minimal( st.text(), lambda s: len(s) >= 10, settings=settings(backend="prng", database=None), ) assert len(s) == 10 def test_backend_can_shrink_booleans(): with temp_register_backend("prng", PrngProvider): b = minimal( st.booleans(), lambda b: b, settings=settings(backend="prng", database=None) ) assert b def test_backend_can_shrink_floats(): with temp_register_backend("prng", PrngProvider): f = minimal( st.floats(), lambda f: f >= 100.5, settings=settings(backend="prng", database=None), ) assert f == 101.0 # mostly a shoehorned coverage test until the shrinker is migrated to the ir # and calls cached_test_function with backends consistently. @given(nodes()) def test_new_conjecture_data_with_backend(node): def test(data): getattr(data, f"draw_{node.type}")(**node.constraints) with temp_register_backend("prng", PrngProvider): runner = ConjectureRunner(test, settings=settings(backend="prng")) runner.cached_test_function([node.value]) # trivial provider for tests which don't care about drawn distributions. class TrivialProvider(PrimitiveProvider): def draw_integer(self, *args, **constraints): return 1 def draw_boolean(self, *args, **constraints): return True def draw_float(self, *args, **constraints): return 1.0 def draw_bytes(self, *args, **constraints): return b"" def draw_string(self, *args, **constraints): return "" class InvalidLifetime(TrivialProvider): lifetime = "forever and a day" def test_invalid_lifetime(): with ( temp_register_backend("invalid_lifetime", InvalidLifetime), pytest.raises(InvalidArgument), ): ConjectureRunner(lambda: True, settings=settings(backend="invalid_lifetime")) function_lifetime_init_count = 0 class LifetimeTestFunction(TrivialProvider): lifetime = "test_function" def __init__(self, conjecturedata): super().__init__(conjecturedata) # hacky, but no easy alternative. global function_lifetime_init_count function_lifetime_init_count += 1 def test_function_lifetime(): with temp_register_backend("lifetime_function", LifetimeTestFunction): @given(st.integers()) @settings(backend="lifetime_function") def test_function(n): pass assert function_lifetime_init_count == 0 test_function() assert function_lifetime_init_count == 1 test_function() assert function_lifetime_init_count == 2 test_case_lifetime_init_count = 0 class LifetimeTestCase(TrivialProvider): lifetime = "test_case" def __init__(self, conjecturedata): super().__init__(conjecturedata) global test_case_lifetime_init_count test_case_lifetime_init_count += 1 def test_case_lifetime(): test_function_count = 0 with temp_register_backend("lifetime_case", LifetimeTestCase): @given(st.integers()) @settings(backend="lifetime_case", database=InMemoryExampleDatabase()) def test_function(n): nonlocal test_function_count test_function_count += 1 assert test_case_lifetime_init_count == 0 test_function() # we create a new provider each time we *try* to generate an input to the # test function, but this could be filtered out, discarded as duplicate, # etc. We also sometimes try predetermined inputs to the test function, # such as ChoiceTemplate(type="simplest"), which does not entail creating # providers. These two facts combined mean that the number of inits could be # anywhere reasonably close to the number of function calls. assert ( test_function_count - 10 <= test_case_lifetime_init_count <= test_function_count + 10 ) def test_flaky_with_backend(): with temp_register_backend("trivial", TrivialProvider), capture_observations(): calls = 0 @given(st.integers()) @settings(backend="trivial", database=None) def test_function(n): nonlocal calls calls += 1 assert n != calls % 2 with pytest.raises(Flaky): test_function() class BadRealizeProvider(TrivialProvider): def realize(self, value, *, for_failure=False): return None def test_bad_realize(): with temp_register_backend("bad_realize", BadRealizeProvider): @given(st.integers()) @settings(backend="bad_realize") def test_function(n): pass with pytest.raises( HypothesisException, match=r"expected .* from BadRealizeProvider.realize", ): test_function() class RealizeProvider(TrivialProvider): # self-documenting constant REALIZED = 42 avoid_realization = True def realize(self, value, *, for_failure=False): if isinstance(value, int): return self.REALIZED return value def test_realize(): with temp_register_backend("realize", RealizeProvider): values = [] @given(st.integers()) @settings(backend="realize") def test_function(n): values.append(current_build_context().data.provider.realize(n)) test_function() # first draw is 0 from ChoiceTemplate(type="simplest") assert values[0] == 0 assert all(n == RealizeProvider.REALIZED for n in values[1:]) def test_realize_dependent_draw(): with temp_register_backend("realize", RealizeProvider): @given(st.data()) @settings(backend="realize") def test_function(data): n1 = data.draw(st.integers()) n2 = data.draw(st.integers(n1, n1 + 10)) assert n1 <= n2 test_function() @pytest.mark.parametrize("verbosity", [Verbosity.verbose, Verbosity.debug]) def test_realization_with_verbosity(verbosity): with temp_register_backend("realize", RealizeProvider): @given(st.floats()) @settings(backend="realize", verbosity=verbosity) def test_function(f): pass with capture_out() as out: test_function() assert "Trying example: test_function(\n f=,\n)" in out.getvalue() @pytest.mark.parametrize("verbosity", [Verbosity.verbose, Verbosity.debug]) def test_realization_with_verbosity_draw(verbosity): with temp_register_backend("realize", RealizeProvider): @given(st.data()) @settings(backend="realize", verbosity=verbosity) def test_function(data): data.draw(st.integers()) with capture_out() as out: test_function() assert "Draw 1: " in out.getvalue() def test_realization_with_observability(): with temp_register_backend("realize", RealizeProvider): @given(st.data()) @settings(backend="realize") def test_function(data): data.draw(st.integers()) with capture_observations() as observations: test_function() test_cases = [tc for tc in observations if tc.type == "test_case"] assert {tc.representation for tc in test_cases} == { # from the first ChoiceTemplate(type="simplest") example "test_function(\n data=data(...),\n)\nDraw 1: 0", # from all other examples. data= isn't ideal; we should special # case this as data=data(...). f"test_function(\n data=,\n)\nDraw 1: {RealizeProvider.REALIZED}", } class ObservableProvider(TrivialProvider): def observe_test_case(self): return {"msg_key": "some message", "data_key": [1, "2", {}]} def observe_information_messages(self, *, lifetime): if lifetime == "test_case": yield {"type": "info", "title": "trivial-data", "content": {"k2": "v2"}} else: assert lifetime == "test_function" yield {"type": "alert", "title": "Trivial alert", "content": "message here"} yield {"type": "info", "title": "trivial-data", "content": {"k2": "v2"}} def test_custom_observations_from_backend(): with temp_register_backend("observable", ObservableProvider): @given(st.booleans()) @settings(backend="observable", database=None) def test_function(_): pass with capture_observations() as ls: test_function() assert len(ls) >= 3 cases = [t.metadata.backend for t in ls if t.type == "test_case"] assert {"msg_key": "some message", "data_key": [1, "2", {}]} in cases infos = [ {k: v for k, v in dataclasses.asdict(t).items() if k in ("title", "content")} for t in ls if t.type != "test_case" ] assert {"title": "Trivial alert", "content": "message here"} in infos assert {"title": "trivial-data", "content": {"k2": "v2"}} in infos class NeverProceedsObservable(ObservableProvider): def realize(self, value, *, for_failure=False): raise BackendCannotProceed def test_custom_observations_cannot_realize(): with temp_register_backend("never_proceeds", NeverProceedsObservable): @given(st.integers()) @settings(backend="never_proceeds", database=None) def test_function(_): pass with capture_observations() as ls: test_function() assert "" in repr(ls) def test_backend_realize_cannot_proceed_increments_invalid(): def test(data): data.draw_integer() with with_register_backend("never_proceeds", NeverProceedsObservable): runner = ConjectureRunner( test, settings=settings(backend="never_proceeds", database=None), ) assert runner.invalid_examples == 0 runner.cached_test_function([1]) assert runner.invalid_examples == 1 def test_backend_realize_cannot_proceed_exception_increments_invalid(): # test the alternative code path where the test is interesting and we realize # the values with for_failure=True def test(data): data.draw_integer() raise ValueError with with_register_backend("never_proceeds", NeverProceedsObservable): runner = ConjectureRunner( test, settings=settings(backend="never_proceeds", database=None), ) assert runner.invalid_examples == 0 runner.cached_test_function([1]) assert runner.invalid_examples == 1 class FallibleProvider(TrivialProvider): def __init__(self, conjecturedata: "ConjectureData", /) -> None: super().__init__(conjecturedata) self._it = itertools.cycle([1, 1, "discard_test_case", "other"]) def draw_integer(self, *args, **constraints): x = next(self._it) if isinstance(x, str): raise BackendCannotProceed(x) return x def test_falls_back_to_default_backend(): with temp_register_backend("fallible", FallibleProvider): seen_other_ints = False @given(st.integers()) @settings(backend="fallible", database=None, max_examples=100) def test_function(x): nonlocal seen_other_ints seen_other_ints |= x != 1 test_function() assert seen_other_ints # must have swapped backends then def test_can_raise_unsatisfiable_after_falling_back(): with temp_register_backend("fallible", FallibleProvider): @given(st.integers()) @settings( backend="fallible", database=None, max_examples=100, suppress_health_check=[HealthCheck.filter_too_much], ) def test_function(x): assume(x == "unsatisfiable") with pytest.raises(Unsatisfiable): test_function() class ExhaustibleProvider(TrivialProvider): scope = "exhausted" def __init__(self, conjecturedata: "ConjectureData", /) -> None: super().__init__(conjecturedata) self._calls = 0 def draw_integer(self, *args, **constraints): self._calls += 1 if self._calls > 20: # This is complete nonsense of course, so we'll see Hypothesis complain # that we found a problem after the backend reported verification. raise BackendCannotProceed(self.scope) return self._calls class UnsoundVerifierProvider(ExhaustibleProvider): scope = "verified" @pytest.mark.parametrize("provider", [ExhaustibleProvider, UnsoundVerifierProvider]) def test_notes_incorrect_verification(provider): msg = "backend='p' claimed to verify this test passes - please send them a bug report!" with temp_register_backend("p", provider): @given(st.integers()) @settings(backend="p", database=None, max_examples=100) def test_function(x): assert x >= 0 # True from this backend, false in general! with pytest.raises(AssertionError) as ctx: test_function() assert (msg in ctx.value.__notes__) == (provider is UnsoundVerifierProvider) def test_invalid_provider_kw(): with pytest.raises(InvalidArgument, match="got an instance instead"): ConjectureData( random=None, provider=TrivialProvider(None), provider_kw={"one": "two"}, ) def test_available_providers_deprecation(): with pytest.warns(errors.HypothesisDeprecationWarning): from hypothesis.internal.conjecture.data import AVAILABLE_PROVIDERS # noqa with pytest.raises(ImportError): from hypothesis.internal.conjecture.data import does_not_exist # noqa @pytest.mark.parametrize("backend", AVAILABLE_PROVIDERS.keys()) @pytest.mark.parametrize( "strategy", [st.integers(), st.text(), st.floats(), st.booleans(), st.binary()] ) def test_can_generate_from_all_available_providers(backend, strategy): # note: database=InMemoryExampleDatabase() is for compatibility with HypoFuzz # here. @given(strategy) @settings(backend=backend, database=InMemoryExampleDatabase()) def f(x): raise ValueError with ( pytest.raises(ValueError), ( pytest.warns( HypothesisWarning, match="/dev/urandom is not available on windows" ) if backend == "hypothesis-urandom" and WINDOWS else nullcontext() ), ): f() def test_saves_on_fatal_error_with_backend(): with temp_register_backend("trivial", TrivialProvider): db = InMemoryExampleDatabase() @given(st.integers()) @settings(backend="trivial", database=db) def test_function(n): raise BaseException("marker") with pytest.raises(BaseException, match="marker"): test_function() assert len(db.data) == 1 class SoundnessTestProvider(TrivialProvider): def __init__(self, conjecturedata): super().__init__(conjecturedata) self.n = 0 def draw_integer(self, **constraints): self.n += 1 if self.n == 1: return 1 raise BackendCannotProceed("verified") def test_raising_verified_after_failure_is_sound(): # see https://github.com/pschanely/hypothesis-crosshair/issues/31#issuecomment-2852940574 with temp_register_backend("soundness_test", SoundnessTestProvider): @given(st.integers()) @settings(backend="soundness_test", database=None) def f(n): assert n != 1 with pytest.raises(AssertionError) as e: f() # full message as of writing: "backend='soundness_test' claimed to # verify this test passes - please send them a bug report!" assert all("backend" not in note for note in e.value.__notes__) def test_replay_choices(): # trivial covering test provider = TrivialProvider(None) provider.replay_choices([1]) class ObservationProvider(TrivialProvider): add_observability_callback = True def __init__(self, conjecturedata: "ConjectureData", /) -> None: super().__init__(conjecturedata) # calls to per_test_case_context_manager and on_observation alternate, # starting with per_test_case_context_manager self.expected = "per_test_case_context_manager" @contextmanager def per_test_case_context_manager(self): assert self.expected == "per_test_case_context_manager" self.expected = "on_observation" yield def on_observation(self, observation: Observation) -> None: assert self.expected == "on_observation" self.expected = "per_test_case_context_manager" @temp_register_backend("observation", ObservationProvider) def test_on_observation_alternates(): @given(st.integers()) @settings(backend="observation") def f(n): pass f() @temp_register_backend("observation", ObservationProvider) def test_on_observation_alternates_on_failure(): @given(st.integers()) @settings(backend="observation") def f(n): # Hypothesis tries n == 0 first, and if that fails then we don't exercise # any provider-specific paths. if n == 1: raise ValueError("unique identifier") with pytest.raises(ValueError, match="unique identifier"): f() @temp_register_backend("observation", TrivialProvider) def test_on_observation_no_override(): @given(st.integers()) @settings(backend="observation") def f(n): assert _callbacks == {} f() @pytest.mark.parametrize("provider", [HypothesisProvider, PrngProvider]) def test_provider_conformance(provider): with warnings.catch_warnings(): # emitted by available_timezones() from st.timezone_keys() on 3.11+ # with tzdata installed. see https://github.com/python/cpython/issues/137841. # Once cpython fixes this, we can remove this. if sys.version_info >= (3, 11): warnings.simplefilter("ignore", EncodingWarning) run_conformance_test( provider, settings=settings(max_examples=20, stateful_step_count=20) ) # see https://github.com/HypothesisWorks/hypothesis/issues/4462 and discussion # in https://github.com/HypothesisWorks/hypothesis/pull/4470 def test_backend_deadline_exceeded_raised_as_flaky_backend_failure(): with temp_register_backend("trivial", TrivialProvider): @given(st.integers()) @settings(backend="trivial", database=None) def f(n): if isinstance(current_build_context().data.provider, TrivialProvider): time.sleep(1) with pytest.raises(FlakyBackendFailure): f() def test_backend_cannot_proceed_raises_on_invalid_scope(): with pytest.raises(InvalidArgument): BackendCannotProceed("not a valid scope") ================================================ FILE: hypothesis-python/tests/conjecture/test_provider_contract.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import example, given, strategies as st from hypothesis.errors import StopTest from hypothesis.internal.compat import WINDOWS from hypothesis.internal.conjecture.choice import ( choice_equal, choice_from_index, choice_permitted, ) from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.providers import ( BytestringProvider, HypothesisProvider, URandomProvider, ) from hypothesis.internal.intervalsets import IntervalSet from tests.conjecture.common import ( choice_types_constraints, float_constr, integer_constr, nodes, string_constr, ) @example(b"\x00" * 100, [("integer", integer_constr())]) @example(b"\x00" * 100, [("integer", integer_constr(0, 2))]) @example(b"\x00" * 100, [("integer", integer_constr(0, 0))]) @example(b"\x00" * 100, [("integer", integer_constr(min_value=0))]) @example(b"\x00" * 100, [("integer", integer_constr(max_value=2))]) @example(b"\x00" * 100, [("integer", integer_constr(0, 2, weights={0: 0.1}))]) @example(b"\x00" * 100, [("boolean", {"p": 1.0})]) @example(b"\x00" * 100, [("boolean", {"p": 0.0})]) @example(b"\x00" * 100, [("boolean", {"p": 1e-99})]) @example(b"\x00" * 100, [("string", string_constr(IntervalSet.from_string("a")))]) @example(b"\x00" * 100, [("float", float_constr())]) @example(b"\x00" * 100, [("bytes", {"min_size": 0, "max_size": 10})]) @example(b"\x00", [("integer", integer_constr())]) @given(st.binary(min_size=200), st.lists(choice_types_constraints())) def test_provider_contract_bytestring(bytestring, choice_type_and_constraints): data = ConjectureData( random=None, observer=None, provider=BytestringProvider, provider_kw={"bytestring": bytestring}, ) for choice_type, constraints in choice_type_and_constraints: # for the threading ci tests constraints = constraints.copy() try: value = getattr(data, f"draw_{choice_type}")(**constraints) except StopTest: return assert choice_permitted(value, constraints) constraints["forced"] = choice_from_index(0, choice_type, constraints) assert choice_equal( constraints["forced"], getattr(data, f"draw_{choice_type}")(**constraints) ) @pytest.mark.parametrize( "provider", [ pytest.param( URandomProvider, marks=pytest.mark.skipif( WINDOWS, reason="/dev/urandom not available on windows" ), ), HypothesisProvider, ], ) @given(st.lists(nodes()), st.randoms()) def test_provider_contract(provider, nodes, random): data = ConjectureData(random=random, provider=provider) for node in nodes: value = getattr(data, f"draw_{node.type}")(**node.constraints) assert choice_permitted(value, node.constraints) ================================================ FILE: hypothesis-python/tests/conjecture/test_shrinker.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import time import pytest from hypothesis import HealthCheck, assume, example, given, settings, strategies as st from hypothesis.internal.conjecture.data import ChoiceNode, ConjectureData from hypothesis.internal.conjecture.datatree import compute_max_children from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.conjecture.shrinker import Shrinker, ShrinkPass, StopShrinking from hypothesis.internal.conjecture.shrinking.common import Shrinker as ShrinkerPass from hypothesis.internal.conjecture.utils import Sampler from hypothesis.internal.floats import MAX_PRECISE_INTEGER from tests.common.utils import skipif_time_unpatched from tests.conjecture.common import ( SOME_LABEL, float_constr, interesting_origin, nodes, nodes_inline, run_to_nodes, shrinking_from, ) @pytest.mark.parametrize("n", [1, 5, 8, 15]) def test_can_shrink_variable_draws_with_just_deletion(n): @shrinking_from((n,) + (0,) * (n - 1) + (1,)) def shrinker(data: ConjectureData): n = data.draw_integer(0, 2**4 - 1) b = [data.draw_integer(0, 2**8 - 1) for _ in range(n)] if any(b): data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_individual_choices)]) assert shrinker.choices == (1, 1) def test_deletion_and_lowering_fails_to_shrink(monkeypatch): monkeypatch.setattr( Shrinker, "shrink", lambda self: self.fixate_shrink_passes( [ShrinkPass(self.minimize_individual_choices)] ), ) monkeypatch.setattr( ConjectureRunner, "generate_new_examples", lambda runner: runner.cached_test_function((b"\0",) * 10), ) @run_to_nodes def nodes(data): for _ in range(10): data.draw_bytes(1, 1) data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (b"\0",) * 10 def test_duplicate_nodes_that_go_away(): @shrinking_from((1234567, 1234567) + (b"\1",) * (1234567 & 255)) def shrinker(data: ConjectureData): x = data.draw_integer(min_value=0) y = data.draw_integer(min_value=0) if x != y: data.mark_invalid() b = [data.draw_bytes(1, 1) for _ in range(x & 255)] if len(set(b)) <= 1: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_duplicated_choices)]) assert shrinker.shrink_target.choices == (0, 0) def test_accidental_duplication(): @shrinking_from((12, 12) + (b"\2",) * 12) def shrinker(data: ConjectureData): x = data.draw_integer(0, 2**8 - 1) y = data.draw_integer(0, 2**8 - 1) if x != y: data.mark_invalid() if x < 5: data.mark_invalid() b = [data.draw_bytes(1, 1) for _ in range(x)] if len(set(b)) == 1: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_duplicated_choices)]) print(shrinker.choices) assert shrinker.choices == (5, 5, *([b"\x00"] * 5)) def test_can_zero_subintervals(): @shrinking_from((3, 0, 0, 0, 1) * 10) def shrinker(data: ConjectureData): for _ in range(10): data.start_span(SOME_LABEL) n = data.draw_integer(0, 2**8 - 1) for _ in range(n): data.draw_integer(0, 2**8 - 1) data.stop_span() if data.draw_integer(0, 2**8 - 1) != 1: return data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.choices == (0, 1) * 10 def test_can_pass_to_an_indirect_descendant(): def tree(data): data.start_span(label=1) n = data.draw_integer(0, 1) data.draw_integer(0, 2**8 - 1) if n: tree(data) tree(data) data.stop_span(discard=True) initial = (1, 10, 0, 0, 1, 0, 0, 10, 0, 0) target = (0, 10) good = {initial, target} @shrinking_from(initial) def shrinker(data: ConjectureData): tree(data) if data.choices in good: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.pass_to_descendant)]) assert shrinker.choices == target def test_shrinking_blocks_from_common_offset(): @shrinking_from((11, 10)) def shrinker(data: ConjectureData): m = data.draw_integer(0, 2**8 - 1) n = data.draw_integer(0, 2**8 - 1) if abs(m - n) <= 1 and max(m, n) > 0: data.mark_interesting(interesting_origin()) shrinker.mark_changed(0) shrinker.mark_changed(1) shrinker.lower_common_node_offset() assert shrinker.choices in {(0, 1), (1, 0)} def test_handle_empty_draws(): @run_to_nodes def nodes(data): while True: data.start_span(SOME_LABEL) n = data.draw_integer(0, 1) data.start_span(SOME_LABEL) data.stop_span() data.stop_span(discard=n > 0) if not n: break data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (0,) def test_can_reorder_spans(): # grouped by iteration: (1, 1) (1, 1) (0) (0) (0) @shrinking_from((1, 1, 1, 1, 0, 0, 0)) def shrinker(data: ConjectureData): total = 0 for _ in range(5): data.start_span(label=0) if data.draw_integer(0, 2**8 - 1): total += data.draw_integer(0, 2**9 - 1) data.stop_span() if total == 2: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.reorder_spans)]) assert shrinker.choices == (0, 0, 0, 1, 1, 1, 1) def test_permits_but_ignores_raising_order(monkeypatch): monkeypatch.setattr( ConjectureRunner, "generate_new_examples", lambda runner: runner.cached_test_function((1,)), ) monkeypatch.setattr( Shrinker, "shrink", lambda self: self.consider_new_nodes(nodes_inline(2)) ) @run_to_nodes def nodes(data): data.draw_integer(0, 3) data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (1,) def test_node_deletion_can_delete_short_ranges(): @shrinking_from([v for i in range(5) for _ in range(i + 1) for v in [i]]) def shrinker(data: ConjectureData): while True: n = data.draw_integer(0, 2**16 - 1) for _ in range(n): if data.draw_integer(0, 2**16 - 1) != n: data.mark_invalid() if n == 4: data.mark_interesting(interesting_origin()) passes = [shrinker.node_program("X" * i) for i in range(1, 5)] shrinker.fixate_shrink_passes(passes) assert shrinker.choices == (4,) * 5 def test_dependent_block_pairs_is_up_to_shrinking_integers(): # Unit test extracted from a failure in tests/nocover/test_integers.py distribution = Sampler([4.0, 8.0, 1.0, 1.0, 0.5]) sizes = [8, 16, 32, 64, 128] @shrinking_from((3, True, 65538, 1)) def shrinker(data: ConjectureData): size = sizes[distribution.sample(data)] result = data.draw_integer(0, 2**size - 1) sign = (-1) ** (result & 1) result = (result >> 1) * sign cap = data.draw_integer(0, 2**8 - 1) if result >= 32768 and cap == 1: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_individual_choices)]) assert shrinker.choices == (1, True, 65536, 1) def test_finding_a_minimal_balanced_binary_tree(): # Tests iteration while the shape of the thing being iterated over can # change. In particular the current example can go from trivial to non # trivial. def tree(data): # Returns height of a binary tree and whether it is height balanced. data.start_span(label=0) if not data.draw_boolean(): result = (1, True) else: h1, b1 = tree(data) h2, b2 = tree(data) result = (1 + max(h1, h2), b1 and b2 and abs(h1 - h2) <= 1) data.stop_span() return result # Starting from an unbalanced tree of depth six @shrinking_from((True,) * 5 + (False,) * 6) def shrinker(data: ConjectureData): _, b = tree(data) if not b: data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.choices == (True, False, True, False, True, False, False) def test_node_programs_are_adaptive(): @shrinking_from((False,) * 1000 + (True,)) def shrinker(data: ConjectureData): while not data.draw_boolean(): pass data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([shrinker.node_program("X")]) assert len(shrinker.choices) == 1 assert shrinker.calls <= 60 def test_zero_examples_with_variable_min_size(): @shrinking_from((255,) * 100) def shrinker(data: ConjectureData): any_nonzero = False for i in range(1, 10): any_nonzero |= data.draw_integer(0, 2**i - 1) > 0 if not any_nonzero: data.mark_invalid() data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.choices == (0,) * 8 + (1,) def test_zero_contained_examples(): @shrinking_from((1,) * 8) def shrinker(data: ConjectureData): for _ in range(4): data.start_span(1) if data.draw_integer(0, 2**8 - 1) == 0: data.mark_invalid() data.start_span(1) data.draw_integer(0, 2**8 - 1) data.stop_span() data.stop_span() data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.choices == (1, 0) * 4 def test_zig_zags_quickly(): @shrinking_from((255,) * 4) def shrinker(data: ConjectureData): m = data.draw_integer(0, 2**16 - 1) n = data.draw_integer(0, 2**16 - 1) if m == 0 or n == 0: data.mark_invalid() if abs(m - n) <= 1: data.mark_interesting(interesting_origin(0)) # Two different interesting origins for avoiding slipping in the # shrinker. if abs(m - n) <= 10: data.mark_interesting(interesting_origin(1)) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_individual_choices)]) assert shrinker.engine.valid_examples <= 100 assert shrinker.choices == (1, 1) @pytest.mark.parametrize( "min_value, max_value, forced, shrink_towards, expected", [ # this test disallows interesting values in radius 10 interval around shrink_towards # to avoid trivial shrinks messing with things, which is why the expected # values are ±10 from shrink_towards. (-100, 0, -100, 0, (-10, -10)), (-100, 0, -100, -35, (-25, -25)), (0, 100, 100, 0, (10, 10)), (0, 100, 100, 65, (75, 75)), ], ) def test_zig_zags_quickly_with_shrink_towards( min_value, max_value, forced, shrink_towards, expected ): # we should be able to efficiently incorporate shrink_towards when dealing # with zig zags. @shrinking_from((forced,) * 2) def shrinker(data: ConjectureData): m = data.draw_integer(min_value, max_value, shrink_towards=shrink_towards) n = data.draw_integer(min_value, max_value, shrink_towards=shrink_towards) # avoid trivial counterexamples if abs(m - shrink_towards) < 10 or abs(n - shrink_towards) < 10: data.mark_invalid() if abs(m - n) <= 1: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_individual_choices)]) assert shrinker.engine.valid_examples <= 40 assert shrinker.choices == expected def test_zero_irregular_examples(): @shrinking_from((255,) * 6) def shrinker(data: ConjectureData): data.start_span(1) data.draw_integer(0, 2**8 - 1) data.draw_integer(0, 2**16 - 1) data.stop_span() data.start_span(1) interesting = ( data.draw_integer(0, 2**8 - 1) > 0 and data.draw_integer(0, 2**16 - 1) > 0 ) data.stop_span() if interesting: data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.choices == (0,) * 2 + (1, 1) def test_retain_end_of_buffer(): @shrinking_from((1, 2, 3, 4, 5, 6, 0)) def shrinker(data: ConjectureData): interesting = False while True: n = data.draw_integer(0, 2**8 - 1) if n == 6: interesting = True if n == 0: break if interesting: data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.choices == (6, 0) def test_can_expand_zeroed_region(): @shrinking_from((255,) * 5) def shrinker(data: ConjectureData): seen_non_zero = False for _ in range(5): if data.draw_integer(0, 2**8 - 1) == 0: if seen_non_zero: data.mark_invalid() else: seen_non_zero = True data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.choices == (0,) * 5 def test_can_expand_deleted_region(): @shrinking_from((1, 2, 3, 4, 0, 0)) def shrinker(data: ConjectureData): def t(): data.start_span(1) data.start_span(1) m = data.draw_integer(0, 2**8 - 1) data.stop_span() data.start_span(1) n = data.draw_integer(0, 2**8 - 1) data.stop_span() data.stop_span() return (m, n) v1 = t() if v1 == (1, 2): if t() != (3, 4): data.mark_invalid() if v1 == (0, 0) or t() == (0, 0): data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.choices == (0, 0) @skipif_time_unpatched def test_will_terminate_stalled_shrinks(): # Suppress the time based slow shrinking check - we only want # the one that checks if we're in a stall where we've shrunk # as far as we're going to get. time.freeze() @shrinking_from((255,) * 100) def shrinker(data: ConjectureData): count = 0 for _ in range(100): if data.draw_integer(0, 2**8 - 1) != 255: count += 1 if count >= 10: return data.mark_interesting(interesting_origin()) shrinker.shrink() assert shrinker.calls <= 1 + 2 * shrinker.max_stall def test_will_let_fixate_shrink_passes_do_a_full_run_through(): @shrinking_from(list(range(50))) def shrinker(data: ConjectureData): for i in range(50): if data.draw_integer(0, 2**8 - 1) != i: data.mark_invalid() data.mark_interesting(interesting_origin()) shrinker.max_stall = 5 passes = [shrinker.node_program("X" * i) for i in range(1, 11)] with pytest.raises(StopShrinking): shrinker.fixate_shrink_passes(passes) assert passes[-1].calls > 0 @pytest.mark.parametrize("n_gap", [0, 1, 2]) def test_can_simultaneously_lower_non_duplicated_nearby_integers(n_gap): @shrinking_from((1, 1) + (0,) * n_gap + (2,)) def shrinker(data: ConjectureData): # Block off lowering the whole buffer if data.draw_integer(0, 2**1 - 1) == 0: data.mark_invalid() m = data.draw_integer(0, 2**8 - 1) for _ in range(n_gap): data.draw_integer(0, 2**8 - 1) n = data.draw_integer(0, 2**16 - 1) if n == m + 1: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.lower_integers_together)]) assert shrinker.choices == (1, 0) + (0,) * n_gap + (1,) def test_redistribute_with_forced_node_integer(): @shrinking_from((15, 10)) def shrinker(data: ConjectureData): n1 = data.draw_integer(0, 100) n2 = data.draw_integer(0, 100, forced=10) if n1 + n2 > 20: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.redistribute_numeric_pairs)]) # redistribute_numeric_pairs shouldn't try modifying forced nodes while # shrinking. Since the second draw is forced, this isn't possible to shrink # with just this pass. assert shrinker.choices == (15, 10) @pytest.mark.parametrize("n", [10, 50, 100, 200]) def test_can_quickly_shrink_to_trivial_collection(n): @shrinking_from([b"\x01" * n]) def shrinker(data: ConjectureData): b = data.draw_bytes() if len(b) >= n: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.minimize_individual_choices)]) assert shrinker.choices == (b"\x00" * n,) assert shrinker.calls < 10 def test_alternative_shrinking_will_lower_to_alternate_value(): # We want to reject the first integer value we see when shrinking # this alternative, because it will be the result of transmuting the # bytes value, and we want to ensure that we can find other values # there when we detect the shape change. seen_int = None @shrinking_from((1, b"hello world")) def shrinker(data: ConjectureData): nonlocal seen_int i = data.draw_integer(min_value=0, max_value=1) if i == 1: if data.draw_bytes(): data.mark_interesting(interesting_origin()) else: n = data.draw_integer(0, 100) if n == 0: return if seen_int is None: seen_int = n elif n != seen_int: data.mark_interesting(interesting_origin()) shrinker.initial_coarse_reduction() assert shrinker.choices[0] == 0 class BadShrinker(ShrinkerPass): """ A shrinker that really doesn't do anything at all. This is mostly a covering test for the shrinker interface methods. """ def run_step(self): return def test_silly_shrinker_subclass(): assert BadShrinker.shrink(10, lambda _: True) == 10 numeric_nodes = nodes(choice_types=["integer", "float"]) @given(numeric_nodes, numeric_nodes, st.integers() | st.floats(allow_nan=False)) @example( ChoiceNode( type="float", value=float(MAX_PRECISE_INTEGER - 1), constraints=float_constr(), was_forced=False, ), ChoiceNode( type="float", value=float(MAX_PRECISE_INTEGER - 1), constraints=float_constr(), was_forced=False, ), 0, ) @settings(suppress_health_check=[HealthCheck.filter_too_much]) def test_redistribute_numeric_pairs(node1, node2, stop): assume(node1.value + node2.value > stop) # don't test extreme shrink_towards values, which lead to this test flaking # from floating point errors assume(abs(node1.constraints.get("shrink_towards", 0)) <= 1e10) assume(abs(node2.constraints.get("shrink_towards", 0)) <= 1e10) # avoid exhausting the tree while generating, which causes @shrinking_from's # runner to raise assume( compute_max_children(node1.type, node1.constraints) + compute_max_children(node2.type, node2.constraints) > 2 ) @shrinking_from([node1.value, node2.value]) def shrinker(data: ConjectureData): v1 = getattr(data, f"draw_{node1.type}")(**node1.constraints) v2 = getattr(data, f"draw_{node2.type}")(**node2.constraints) if v1 + v2 > stop: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.redistribute_numeric_pairs)]) assert len(shrinker.choices) == 2 shrink_towards = ( node1.constraints["shrink_towards"] if node1.type == "integer" else 0 ) # we should always have brought the first choice closer to shrink_towards, # or left the choices the same. And the values should sum to where they started. assert abs(shrinker.choices[0] - shrink_towards) <= abs( node1.value - shrink_towards ) assert ( # pytest.approx for differences in floating-point summations pytest.approx(shrinker.choices[0] + shrinker.choices[1], rel=0.001) == node1.value + node2.value ) @pytest.mark.parametrize( "start, expected", [ (("1" * 5, "1" * 5), ("0" * 5, "0" * 5)), (("1222344", "1222344"), ("0" * 7, "0" * 7)), ], ) @pytest.mark.parametrize("gap", [0, 1, 2, 3]) def test_lower_duplicated_characters_across_choices(start, expected, gap): # the draws from `gap` are irrelevant and only test that we can still shrink # duplicated characters from nearby choices even when the choices are not # consecutive. @shrinking_from([start[0], *([0] * gap), start[1]]) def shrinker(data: ConjectureData): s1 = data.draw(st.text()) for _ in range(gap): data.draw(st.integers()) s2 = data.draw(st.text()) if s1 == s2: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.lower_duplicated_characters)]) assert shrinker.choices == (expected[0],) + (0,) * gap + (expected[1],) def test_shrinking_one_of_with_same_shape(): # This is a covering test for our one_of shrinking logic for the case when # the choice sequence *doesn't* change shape in the newly chosen one_of branch. @shrinking_from([1, 0]) def shrinker(data: ConjectureData): n = data.draw_integer(0, 1) data.draw_integer() if n == 1: data.mark_interesting(interesting_origin()) shrinker.initial_coarse_reduction() assert shrinker.choices == (1, 0) @pytest.mark.parametrize("invert", [False, True]) # cover the negative case @pytest.mark.parametrize( "min_value, max_value", [(None, None), (None, 15), (-15, None), (-15, 15)] ) @pytest.mark.parametrize( "shrink_towards, start", [ # straddles shrink_towards (5, (2, 10)), # both below shrink_towards (5, (2, 4)), # both above shrink_towards (5, (8, 10)), # exactly shrink_towards (5, (5, 5)), ], ) def test_redistribute_numeric_pairs_shrink_towards_explicit_integer( min_value, max_value, shrink_towards, start, invert ): if invert: shrink_towards = -shrink_towards start = (-start[1], -start[0]) # redistributing should redistribute towards shrink_towards, not 0 target = start[0] + start[1] @shrinking_from(start) def shrinker(data: ConjectureData): v1 = data.draw_integer( shrink_towards=shrink_towards, min_value=min_value, max_value=max_value ) v2 = data.draw_integer( shrink_towards=shrink_towards, min_value=min_value, max_value=max_value ) if v1 + v2 == target: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.redistribute_numeric_pairs)]) assert shrinker.choices == (shrink_towards, target - shrink_towards) @pytest.mark.parametrize("invert", [False, True]) @pytest.mark.parametrize( "start", [ (2.0, 10.0), (2.0, 4.0), (8.0, 10.0), (5.0, 5.0), ], ) def test_redistribute_numeric_pairs_shrink_towards_explicit_float(start, invert): if invert: start = (-start[1], -start[0]) target = start[0] + start[1] @shrinking_from(start) def shrinker(data: ConjectureData): v1 = data.draw_float() v2 = data.draw_float() if v1 + v2 == target: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.redistribute_numeric_pairs)]) assert shrinker.choices == (0, target) @pytest.mark.parametrize( "shrink_towards, start", [ (5, (2, 10.0)), (5, (2, 4.0)), (5, (8, 10.0)), (5, (5, 5.0)), ], ) def test_redistribute_numeric_pairs_shrink_towards_explicit_combined( shrink_towards, start ): # test case for one integer and one float draw. No `invert` parametrization # because it moderately complicates things target = start[0] + start[1] @shrinking_from(start) def shrinker(data: ConjectureData): v1 = data.draw_integer(shrink_towards=shrink_towards) v2 = data.draw_float() if v1 + v2 == target: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.redistribute_numeric_pairs)]) assert shrinker.choices == (shrink_towards, target - shrink_towards) @given(st.data(), st.integers(), st.integers()) def test_redistribute_numeric_pairs_shrink_towards_integer( data, target, shrink_towards ): start = data.draw(st.integers(max_value=target)) end = target - start @shrinking_from([start, end]) def shrinker(data: ConjectureData): v1 = data.draw_integer(shrink_towards=shrink_towards) v2 = data.draw_integer(shrink_towards=shrink_towards) if v1 + v2 == target: data.mark_interesting(interesting_origin()) shrinker.fixate_shrink_passes([ShrinkPass(shrinker.redistribute_numeric_pairs)]) assert shrinker.choices == (shrink_towards, target - shrink_towards) ================================================ FILE: hypothesis-python/tests/conjecture/test_shrinking_interface.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from random import Random from hypothesis.internal.conjecture.shrinking import Integer from tests.common.utils import capture_out def test_debug_output(): with capture_out() as o: Integer.shrink(10, lambda x: True, debug=True, random=Random(0)) assert "initial=10" in o.getvalue() assert "shrinking to 0" in o.getvalue() def test_includes_name_in_repr_if_set(): assert ( repr(Integer(10, lambda x: True, name="hi there", random=Random(0))) == "Integer('hi there', initial=10, current=10)" ) def test_normally_contains_no_space_for_name(): assert ( repr(Integer(10, lambda x: True, random=Random(0))) == "Integer(initial=10, current=10)" ) ================================================ FILE: hypothesis-python/tests/conjecture/test_test_data.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import itertools import pytest from hypothesis import strategies as st from hypothesis.errors import Frozen from hypothesis.internal.conjecture.data import ( MAX_DEPTH, ConjectureData, DataObserver, Overrun, Status, StopTest, structural_coverage, ) from hypothesis.strategies import SearchStrategy from tests.conjecture.common import buffer_size_limit, interesting_origin def test_cannot_draw_after_freeze(): d = ConjectureData.for_choices((True,)) d.draw_boolean() d.freeze() with pytest.raises(Frozen): d.draw_boolean() def test_can_double_freeze(): d = ConjectureData.for_choices([]) d.freeze() assert d.frozen d.freeze() assert d.frozen def test_draw_past_end_sets_overflow(): d = ConjectureData.for_choices((True,)) d.draw_boolean() with pytest.raises(StopTest) as e: d.draw_boolean() assert e.value.testcounter == d.testcounter assert d.frozen assert d.status == Status.OVERRUN def test_notes_repr(): d = ConjectureData.for_choices([]) d.note(b"hi") assert repr(b"hi") in d.output def test_can_mark_interesting(): d = ConjectureData.for_choices([]) with pytest.raises(StopTest): d.mark_interesting(interesting_origin()) assert d.frozen assert d.status == Status.INTERESTING def test_can_mark_invalid(): d = ConjectureData.for_choices([]) with pytest.raises(StopTest): d.mark_invalid() assert d.frozen assert d.status == Status.INVALID def test_can_mark_invalid_with_why(): d = ConjectureData.for_choices([]) with pytest.raises(StopTest): d.mark_invalid("some reason") assert d.frozen assert d.status == Status.INVALID assert d.events == {"invalid because": "some reason"} class BoomStrategy(SearchStrategy): def do_draw(self, data): data.draw_boolean() raise ValueError def test_closes_interval_on_error_in_strategy(): d = ConjectureData.for_choices((True,)) with pytest.raises(ValueError): d.draw(BoomStrategy()) d.freeze() assert not any(eg.end is None for eg in d.spans) class BigStrategy(SearchStrategy): def do_draw(self, data): data.draw_bytes(10**6, 10**6) def test_does_not_double_freeze_in_interval_close(): d = ConjectureData.for_choices((b"hi",)) with pytest.raises(StopTest): d.draw(BigStrategy()) assert d.frozen assert not any(eg.end is None for eg in d.spans) def test_triviality(): d = ConjectureData.for_choices((True, False, b"1")) d.start_span(label=1) d.draw(st.booleans()) d.draw(st.booleans()) d.stop_span() d.start_span(label=2) d.draw_bytes(1, 1, forced=bytes([2])) d.stop_span() d.freeze() def trivial(u, v): ex = next(ex for ex in d.spans if ex.start == u and ex.end == v) return all(node.trivial for node in d.nodes[ex.start : ex.end]) assert not trivial(0, 2) assert not trivial(0, 1) assert trivial(1, 2) assert trivial(2, 3) def test_example_depth_marking(): d = ConjectureData.for_choices((0,) * 6) d.draw(st.integers()) # v1 d.start_span(0) d.draw(st.integers()) # v2 d.draw(st.integers()) # v3 d.stop_span() d.draw(st.integers()) # v4 d.freeze() assert len(d.spans) == 6 depths = [(ex.choice_count, ex.depth) for ex in d.spans] assert depths == [ (4, 0), # top (1, 1), # v1 (2, 1), # inner (1, 2), # v2 (1, 2), # v3 (1, 1), # v4 ] def test_has_examples_even_when_empty(): d = ConjectureData.for_choices([]) d.draw(st.just(False)) d.freeze() assert d.spans def test_has_cached_examples_even_when_overrun(): d = ConjectureData.for_choices((False,)) d.start_span(3) d.draw_boolean() d.stop_span() try: d.draw_boolean() except StopTest: pass assert d.status == Status.OVERRUN assert any(ex.label == 3 and ex.choice_count == 1 for ex in d.spans) assert d.spans is d.spans def test_can_observe_draws(): class LoggingObserver(DataObserver): def __init__(self): self.log = [] def draw_boolean(self, value: bool, *, was_forced: bool, constraints: dict): self.log.append(("draw_boolean", value, was_forced)) def draw_integer(self, value: int, *, was_forced: bool, constraints: dict): self.log.append(("draw_integer", value, was_forced)) def conclude_test(self, *args): assert d.frozen self.log.append(("concluded", *args)) observer = LoggingObserver() d = ConjectureData.for_choices((True, 1, 3), observer=observer) origin = interesting_origin() d.draw_boolean() d.draw_integer(0, 2**7 - 1, forced=10) d.draw_integer(0, 2**8 - 1) with pytest.raises(StopTest): d.conclude_test(Status.INTERESTING, interesting_origin=origin) assert observer.log == [ ("draw_boolean", True, False), ("draw_integer", 10, True), ("draw_integer", 3, False), ("concluded", Status.INTERESTING, origin), ] def test_calls_concluded_implicitly(): class NoteConcluded(DataObserver): def conclude_test(self, status, reason): assert d.frozen self.conclusion = (status, reason) observer = NoteConcluded() d = ConjectureData.for_choices((True,), observer=observer) d.draw_boolean() d.freeze() assert observer.conclusion == (Status.VALID, None) def test_examples_show_up_as_discarded(): d = ConjectureData.for_choices((True, False, True)) d.start_span(1) d.draw_boolean() d.stop_span(discard=True) d.start_span(1) d.draw_boolean() d.stop_span() d.freeze() assert len([ex for ex in d.spans if ex.discarded]) == 1 def test_examples_support_negative_indexing(): d = ConjectureData.for_choices((True, True)) d.draw(st.booleans()) d.draw(st.booleans()) d.freeze() assert d.spans[-1].choice_count == 1 def test_examples_out_of_bounds_index(): d = ConjectureData.for_choices((False,)) d.draw(st.booleans()) d.freeze() with pytest.raises(IndexError): d.spans[10] def test_can_override_label(): d = ConjectureData.for_choices((False,)) d.draw(st.booleans(), label=7) d.freeze() assert any(ex.label == 7 for ex in d.spans) def test_will_mark_too_deep_examples_as_invalid(): d = ConjectureData.for_choices((0,)) s = st.integers() for _ in range(MAX_DEPTH + 1): s = s.map(lambda x: None) with pytest.raises(StopTest): d.draw(s) assert d.status == Status.INVALID def test_empty_strategy_is_invalid(): d = ConjectureData.for_choices([]) with pytest.raises(StopTest): d.draw(st.nothing()) assert d.status == Status.INVALID def test_can_note_non_str(): d = ConjectureData.for_choices([]) x = object() d.note(x) assert repr(x) in d.output def test_can_note_str_as_non_repr(): d = ConjectureData.for_choices([]) d.note("foo") assert d.output == "foo" def test_result_is_overrun(): d = ConjectureData.for_choices([]) with pytest.raises(StopTest): d.draw_boolean() assert d.as_result() is Overrun def test_trivial_before_force_agrees_with_trivial_after(): d = ConjectureData.for_choices((False, True, True)) d.draw_boolean() d.draw_boolean(forced=True) d.draw_boolean() t1 = [d.nodes[i].trivial for i in range(3)] d.freeze() r = d.as_result() t2 = [n.trivial for n in r.nodes] t3 = [r.nodes[i].trivial for i in range(3)] assert t1 == t2 == t3 def test_events_are_noted(): d = ConjectureData.for_choices([]) d.events["hello"] = "" assert "hello" in d.events def test_child_indices(): d = ConjectureData.for_choices((True,) * 4) d.start_span(0) # examples[1] d.start_span(1) # examples[2] d.draw(st.booleans()) # examples[3] (st.booleans) d.draw(st.booleans()) # examples[4] (st.booleans) d.stop_span() d.stop_span() d.draw(st.booleans()) # examples[5] (st.booleans) d.draw(st.booleans()) # examples[6] (st.booleans) d.freeze() assert list(d.spans.children[0]) == [1, 5, 6] assert list(d.spans.children[1]) == [2] assert list(d.spans.children[2]) == [3, 4] assert d.spans[0].parent is None for ex in list(d.spans)[1:]: assert ex in d.spans[ex.parent].children def test_example_equality(): d = ConjectureData.for_choices((False, False)) d.start_span(0) d.draw_boolean() d.stop_span() d.start_span(0) d.draw_boolean() d.stop_span() d.freeze() examples = list(d.spans) for ex1, ex2 in itertools.combinations(examples, 2): assert ex1 != ex2 assert not (ex1 == ex2) # noqa for ex in examples: assert ex == ex assert not (ex != ex) # noqa assert not (ex == "hello") # noqa assert ex != "hello" def test_structural_coverage_is_cached(): assert structural_coverage(50) is structural_coverage(50) def test_examples_create_structural_coverage(): data = ConjectureData.for_choices([]) data.start_span(42) data.stop_span() data.freeze() assert structural_coverage(42) in data.tags def test_discarded_examples_do_not_create_structural_coverage(): data = ConjectureData.for_choices([]) data.start_span(42) data.stop_span(discard=True) data.freeze() assert structural_coverage(42) not in data.tags def test_children_of_discarded_examples_do_not_create_structural_coverage(): data = ConjectureData.for_choices([]) data.start_span(10) data.start_span(42) data.stop_span() data.stop_span(discard=True) data.freeze() assert structural_coverage(42) not in data.tags assert structural_coverage(10) not in data.tags def test_overruns_at_exactly_max_length(): with buffer_size_limit(1): data = ConjectureData(prefix=[True], random=None, max_choices=1) data.draw_boolean() try: data.draw_boolean() except StopTest: pass assert data.status is Status.OVERRUN ================================================ FILE: hypothesis-python/tests/conjecture/test_utils.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from fractions import Fraction import pytest from hypothesis import ( HealthCheck, Phase, assume, example, given, settings, strategies as st, ) from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData, Status, StopTest from hypothesis.internal.coverage import IN_COVERAGE_TESTS try: import numpy as np except ImportError: np = None def test_drawing_certain_coin_still_writes(): data = ConjectureData.for_choices([True]) assert data.draw_boolean(1) assert data.choices == (True,) def test_drawing_impossible_coin_still_writes(): data = ConjectureData.for_choices([False]) assert not data.draw_boolean(0) assert data.choices == (False,) def test_draws_extremely_small_p(): data = ConjectureData.for_choices((True,)) assert data.draw_boolean(0.5**65) @example([Fraction(1, 3), Fraction(1, 3), Fraction(1, 3)]) @example([Fraction(1, 1), Fraction(1, 2)]) @example([Fraction(1, 2), Fraction(4, 10)]) @example([Fraction(1, 1), Fraction(3, 5), Fraction(1, 1)]) @example([Fraction(2, 257), Fraction(2, 5), Fraction(1, 11)]) @example([0, 2, 47]) @settings( deadline=None, suppress_health_check=list(HealthCheck), phases=[Phase.explicit] if IN_COVERAGE_TESTS else settings.default.phases, ) @given(st.lists(st.fractions(min_value=0, max_value=1), min_size=1)) def test_sampler_distribution(weights): total = sum(weights) n = len(weights) assume(total > 0) probabilities = [w / total for w in weights] sampler = cu.Sampler(weights) calculated = [Fraction(0)] * n for base, alternate, p_alternate in sampler.table: calculated[base] += (1 - p_alternate) / n calculated[alternate] += p_alternate / n for expected, actual in zip(probabilities, calculated, strict=True): if isinstance(actual, Fraction): assert expected == actual else: assert abs(expected - actual) < 0.001 def test_sampler_does_not_draw_minimum_if_zero(): sampler = cu.Sampler([0, 2, 47]) assert sampler.sample(ConjectureData.for_choices([0, 0])) != 0 def test_sampler_shrinks(): sampler = cu.Sampler([4.0, 8.0, 1.0, 1.0, 0.5]) assert sampler.sample(ConjectureData.for_choices([0] * 3)) == 0 def test_can_force_sampler(): sampler = cu.Sampler([0.5, 0.5]) cd = ConjectureData.for_choices([0] * 100) assert sampler.sample(cd, forced=0) == 0 assert sampler.sample(cd, forced=1) == 1 def test_combine_labels_is_distinct(): x = 10 y = 100 assert cu.combine_labels(x, y) not in (x, y) @given(st.integers()) def test_combine_labels_is_identity_for_single_argument(n): assert cu.combine_labels(n) == n @pytest.mark.skipif(np is None, reason="requires Numpy") def test_invalid_numpy_sample(): with pytest.raises(InvalidArgument): cu.check_sample(np.array([[1, 1], [1, 1]]), "array") @pytest.mark.skipif(np is None, reason="requires Numpy") def test_valid_numpy_sample(): cu.check_sample(np.array([1, 2, 3]), "array") def test_invalid_set_sample(): with pytest.raises(InvalidArgument): cu.check_sample({1, 2, 3}, "array") def test_valid_list_sample(): cu.check_sample([1, 2, 3], "array") def test_choice(): assert ConjectureData.for_choices([1]).choice([1, 2, 3]) == 2 def test_fixed_size_draw_many(): many = cu.many( ConjectureData.for_choices([]), min_size=3, max_size=3, average_size=3 ) assert many.more() assert many.more() assert many.more() assert not many.more() def test_astronomically_unlikely_draw_many(): # Our internal helper doesn't underflow to zero or negative, but nor # will we ever generate an element for such a low average size. data = ConjectureData.for_choices((True,) * 1000) many = cu.many(data, min_size=0, max_size=10, average_size=1e-5) assert many.more() def test_rejection_eventually_terminates_many(): many = cu.many( ConjectureData.for_choices((True,) * 1000), min_size=0, max_size=1000, average_size=100, ) count = 0 while many.more(): count += 1 many.reject() assert count <= 100 def test_rejection_eventually_terminates_many_invalid_for_min_size(): data = ConjectureData.for_choices((True,) * 1000) many = cu.many(data, min_size=1, max_size=1000, average_size=100) with pytest.raises(StopTest): while many.more(): many.reject() assert data.status == Status.INVALID def test_many_with_min_size(): many = cu.many( ConjectureData.for_choices((False,) * 5), min_size=2, average_size=10, max_size=1000, ) assert many.more() assert many.more() assert not many.more() def test_many_with_max_size(): many = cu.many( ConjectureData.for_choices((True,) * 5), min_size=0, average_size=1, max_size=2 ) assert many.more() assert many.more() assert not many.more() def test_samples_from_a_range_directly(): s = cu.check_sample(range(10**1000), "") assert isinstance(s, range) def test_p_continue_to_average_saturates(): assert cu._p_continue_to_avg(1.1, 100) == 100 def test_unhashable_calc_label(): class Unhashable: def __call__(self): return None def __hash__(self): raise TypeError c1 = Unhashable() c2 = Unhashable() with pytest.raises(TypeError): assert cu.calc_label_from_hash(c1) == cu.calc_label_from_hash(c2) assert cu.calc_label_from_callable(c1) == cu.calc_label_from_callable(c2) ================================================ FILE: hypothesis-python/tests/cover/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/cover/test_annotations.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from inspect import Parameter as P, signature import pytest from hypothesis import given, strategies as st from hypothesis.internal.reflection import ( convert_positional_arguments, define_function_signature, get_pretty_function_description, ) @given(st.integers()) def test_has_an_annotation(i: int): pass def universal_acceptor(*args, **kwargs): return args, kwargs def has_annotation(a: int, *b, c=2) -> None: pass @pytest.mark.parametrize("f", [has_annotation, lambda *, a: a, lambda *, a=1: a]) def test_copying_preserves_signature(f): af = signature(f) t = define_function_signature("foo", "docstring", af)(universal_acceptor) at = signature(t) assert af.parameters == at.parameters @pytest.mark.parametrize( "lam,source", [ ((lambda *z, a: a), "lambda *z, a: a"), ((lambda *z, a=1: a), "lambda *z, a=1: a"), ((lambda *, a: a), "lambda *, a: a"), ((lambda *, a=1: a), "lambda *, a=1: a"), ((lambda **kw: kw), "lambda **kw: kw"), ], ) def test_kwonly_lambda_formatting(lam, source): # Testing kwonly lambdas, with and without varargs and default values assert get_pretty_function_description(lam) == source def test_given_notices_missing_kwonly_args(): @given(a=st.none()) def reqs_kwonly(*, a, b): pass with pytest.raises(TypeError): reqs_kwonly() reqs_kwonly(b=None) def test_converter_handles_kwonly_args(): def f(*, a, b=2): pass out = convert_positional_arguments(f, (), {"a": 1}) assert out == ((), {"a": 1}) def test_converter_notices_missing_kwonly_args(): def f(*, a, b=2): pass with pytest.raises(TypeError): assert convert_positional_arguments(f, (), {}) def to_wrap_with_composite(draw: None, strat: float, nothing: list) -> int: return draw(st.none()) def return_annot() -> int: return 4 # per RFC 1149.5 / xckd 221 def first_annot(draw: None): pass def test_composite_edits_annotations(): sig_comp = signature(st.composite(to_wrap_with_composite)) assert sig_comp.return_annotation == st.SearchStrategy[int] assert sig_comp.parameters["nothing"].annotation is not P.empty assert "draw" not in sig_comp.parameters @pytest.mark.parametrize("nargs", [1, 2, 3]) def test_given_edits_annotations(nargs): sig_given = signature(given(*(nargs * [st.none()]))(to_wrap_with_composite)) assert sig_given.return_annotation is None assert len(sig_given.parameters) == 3 - nargs assert all(p.annotation is not P.empty for p in sig_given.parameters.values()) ================================================ FILE: hypothesis-python/tests/cover/test_arbitrary_data.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from pytest import raises from hypothesis import find, given, settings, strategies as st from hypothesis.errors import InvalidArgument @given(st.integers(), st.data()) def test_conditional_draw(x, data): y = data.draw(st.integers(min_value=x)) assert y >= x def test_prints_on_failure(): @given(st.data()) def test(data): x = data.draw(st.lists(st.integers(0, 10), min_size=2)) y = data.draw(st.sampled_from(x)) x.remove(y) if y in x: raise ValueError with raises(ValueError) as err: test() assert "Draw 1: [0, 0]" in err.value.__notes__ assert "Draw 2: 0" in err.value.__notes__ def test_prints_labels_if_given_on_failure(): @given(st.data()) def test(data): x = data.draw(st.lists(st.integers(0, 10), min_size=2), label="Some numbers") y = data.draw(st.sampled_from(x), label="A number") assert y in x x.remove(y) assert y not in x with raises(AssertionError) as err: test() assert "Draw 1 (Some numbers): [0, 0]" in err.value.__notes__ assert "Draw 2 (A number): 0" in err.value.__notes__ def test_given_twice_is_same(): @given(st.data(), st.data()) def test(data1, data2): data1.draw(st.integers()) data2.draw(st.integers()) raise ValueError with raises(ValueError) as err: test() assert "Draw 1: 0" in err.value.__notes__ assert "Draw 2: 0" in err.value.__notes__ # `find` doesn't seem to be thread-safe, though I don't actually see why not @pytest.mark.xfail( settings._current_profile == "threading", strict=False, reason="not thread-safe?" ) def test_data_supports_find(): data = find(st.data(), lambda data: data.draw(st.integers()) >= 10) assert data.conjecture_data.choices == (10,) @pytest.mark.parametrize("f", ["filter", "map", "flatmap"]) def test_errors_when_normal_strategy_functions_are_used(f): with raises(InvalidArgument): getattr(st.data(), f)(lambda x: 1) def test_nice_repr(): assert repr(st.data()) == "data()" ================================================ FILE: hypothesis-python/tests/cover/test_asyncio.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import asyncio import warnings from unittest import TestCase import pytest from hypothesis import assume, given, settings, strategies as st from hypothesis.internal.compat import PYPY from tests.common.utils import skipif_emscripten pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason="bad interactions when mixing threads and asyncio", ) def coro_decorator(f): with warnings.catch_warnings(): warnings.simplefilter(action="ignore", category=DeprecationWarning) try: return asyncio.coroutine(f) except AttributeError: pytest.skip("needs fixing for asyncio version", allow_module_level=True) class TestAsyncio(TestCase): timeout = 5 def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() def execute_example(self, f): error = None def g(): nonlocal error try: x = f() if x is not None: yield from x except BaseException as e: error = e coro = coro_decorator(g) future = asyncio.wait_for(coro(), timeout=self.timeout) self.loop.run_until_complete(future) if error is not None: raise error @pytest.mark.skipif(PYPY, reason="Error in asyncio.new_event_loop()") @given(x=st.text()) @coro_decorator def test_foo(self, x): assume(x) yield from asyncio.sleep(0.001) assert x class TestAsyncioRun(TestCase): # In principle, these tests could indeed run on emscripten if we grab the existing # event loop and run them there. However, that seems to have hit an infinite loop # and so we're just skipping them for now and will revisit later. def execute_example(self, f): asyncio.run(f()) @skipif_emscripten @given(x=st.text()) @coro_decorator def test_foo_yield_from(self, x): assume(x) yield from asyncio.sleep(0.001) assert x @skipif_emscripten @given(st.text()) async def test_foo_await(self, x): assume(x) await asyncio.sleep(0.001) assert x ================================================ FILE: hypothesis-python/tests/cover/test_cache_implementation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import threading from functools import partial from random import Random import pytest from hypothesis import ( HealthCheck, assume, example, given, note, settings, strategies as st, ) from hypothesis.errors import InvalidArgument from hypothesis.internal.cache import GenericCache, LRUCache, LRUReusedCache from tests.common.utils import Why, skipif_emscripten, xfail_on_crosshair class LRUCacheAlternative(GenericCache): __slots__ = ("__tick",) def __init__(self, max_size): super().__init__(max_size) self.__tick = 0 def new_entry(self, key, value): return self.tick() def on_access(self, key, value, score): return self.tick() def tick(self): self.__tick += 1 return self.__tick class LFUCache(GenericCache): def new_entry(self, key, value): return 1 def on_access(self, key, value, score): return score + 1 @st.composite def write_pattern(draw, min_distinct_keys=0): keys = draw( st.lists(st.integers(0, 1000), unique=True, min_size=max(min_distinct_keys, 1)) ) values = draw(st.lists(st.integers(), unique=True, min_size=1)) s = st.lists( st.tuples(st.sampled_from(keys), st.sampled_from(values)), min_size=min_distinct_keys, ) if min_distinct_keys > 0: s = s.filter(lambda ls: len({k for k, _ in ls}) >= min_distinct_keys) return draw(s) class ValueScored(GenericCache): def new_entry(self, key, value): return value class RandomCache(GenericCache): def __init__(self, max_size): super().__init__(max_size) self.random = Random(0) def new_entry(self, key, value): return self.random.random() def on_access(self, key, value, score): return self.random.random() @pytest.mark.parametrize( "implementation", [LRUCache, LFUCache, LRUReusedCache, ValueScored, RandomCache, LRUCacheAlternative], ) @example(writes=[(0, 0), (3, 0), (1, 0), (2, 0), (2, 0), (1, 0)], size=4) @example(writes=[(0, 0)], size=1) @example(writes=[(1, 0), (2, 0), (0, -1), (1, 0)], size=3) @given(write_pattern(), st.integers(1, 10)) def test_behaves_like_a_dict_with_losses(implementation, writes, size): model = {} target = implementation(max_size=size) for k, v in writes: try: assert model[k] == target[k] except KeyError: pass model[k] = v target[k] = v target.check_valid() assert target[k] == v for r, s in model.items(): try: assert s == target[r] except KeyError: pass assert len(target) <= min(len(model), size) @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) @settings( suppress_health_check={HealthCheck.too_slow} | set(settings().suppress_health_check), deadline=None, ) @given(write_pattern(min_distinct_keys=2), st.data()) def test_always_evicts_the_lowest_scoring_value(writes, data): scores = {} n_keys = len({k for k, _ in writes}) assume(n_keys > 1) size = data.draw(st.integers(1, n_keys - 1)) evicted = set() def new_score(key): scores[key] = data.draw(st.integers(0, 1000), label=f"scores[{key!r}]") return scores[key] last_entry = None class Cache(GenericCache): def new_entry(self, key, value): nonlocal last_entry last_entry = key evicted.discard(key) assert key not in scores return new_score(key) def on_access(self, key, value, score): assert key in scores return new_score(key) def on_evict(self, key, value, score): note(f"Evicted {key!r}") assert score == scores[key] del scores[key] if len(scores) > 1: assert score <= min(v for k, v in scores.items() if k != last_entry) evicted.add(key) target = Cache(max_size=size) model = {} for k, v in writes: target[k] = v model[k] = v assert evicted assert len(evicted) + len(target) == len(model) assert len(scores) == len(target) for k, v in model.items(): try: assert target[k] == v assert k not in evicted except KeyError: assert k in evicted def test_basic_access(): cache = ValueScored(max_size=2) cache[1] = 0 cache[1] = 0 cache[0] = 1 cache[2] = 0 assert cache[2] == 0 assert cache[0] == 1 assert len(cache) == 2 def test_can_clear_a_cache(): x = ValueScored(1) x[0] = 1 assert len(x) == 1 x.clear() assert len(x) == 0 def test_max_size_must_be_positive(): with pytest.raises(InvalidArgument): ValueScored(max_size=0) def test_pinning_prevents_eviction(): cache = LRUReusedCache(max_size=10) cache.pin(20, 1) for i in range(20): cache[i] = 0 assert cache[20] == 1 def test_unpinning_allows_eviction(): cache = LRUReusedCache(max_size=10) cache.pin(20, True) for i in range(20): cache[i] = False assert 20 in cache cache.unpin(20) cache[21] = False assert 20 not in cache def test_unpins_must_match_pins(): cache = LRUReusedCache(max_size=2) cache.pin(1, 1) assert cache.is_pinned(1) assert cache[1] == 1 cache.pin(1, 2) assert cache.is_pinned(1) assert cache[1] == 2 cache.unpin(1) assert cache.is_pinned(1) assert cache[1] == 2 cache.unpin(1) assert not cache.is_pinned(1) def test_will_error_instead_of_evicting_pin(): cache = LRUReusedCache(max_size=1) cache.pin(1, 1) with pytest.raises(ValueError): cache[2] = 2 assert 1 in cache assert 2 not in cache def test_will_error_for_bad_unpin(): cache = LRUReusedCache(max_size=1) cache[1] = 1 with pytest.raises(ValueError): cache.unpin(1) def test_still_inserts_if_score_is_worse(): class TC(GenericCache): def new_entry(self, key, value): return key cache = TC(1) cache[0] = 1 cache[1] = 1 assert 0 not in cache assert 1 in cache assert len(cache) == 1 def test_does_insert_if_score_is_better(): class TC(GenericCache): def new_entry(self, key, value): return value cache = TC(1) cache[0] = 1 cache[1] = 0 assert 0 not in cache assert 1 in cache assert len(cache) == 1 def test_double_pinning_does_not_add_entry(): cache = LRUReusedCache(2) cache.pin(0, 0) cache.pin(0, 1) cache[1] = 1 assert len(cache) == 2 def test_can_add_new_keys_after_unpinning(): cache = LRUReusedCache(1) cache.pin(0, 0) cache.unpin(0) cache[1] = 1 assert len(cache) == 1 assert 1 in cache def test_iterates_over_remaining_keys(): cache = LRUReusedCache(2) for i in range(3): cache[i] = "hi" assert sorted(cache) == [1, 2] def test_lru_cache_is_actually_lru(): cache = LRUCache(2) cache[1] = 1 # [1] cache[2] = 2 # [1, 2] cache[1] # [2, 1] cache[3] = 2 # [2, 1, 3] -> drop least recently used -> [1, 3] assert list(cache) == [1, 3] @skipif_emscripten def test_cache_is_threadsafe_issue_2433_regression(): errors = [] def target(): for _ in range(1000): try: st.builds(partial(str)) except Exception as exc: errors.append(exc) workers = [threading.Thread(target=target) for _ in range(4)] for worker in workers: worker.start() for worker in workers: worker.join() assert not errors ================================================ FILE: hypothesis-python/tests/cover/test_caching.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import strategies as st from hypothesis.errors import InvalidArgument def test_no_args(): assert st.text() is st.text() def test_tuple_lengths(): assert st.tuples(st.integers()) is st.tuples(st.integers()) assert st.tuples(st.integers()) is not st.tuples(st.integers(), st.integers()) def test_values(): assert st.integers() is not st.integers(min_value=1) def test_alphabet_key(): assert st.text(alphabet="abcs") is st.text(alphabet="abcs") def test_does_not_error_on_unhashable_posarg(): st.text(["a", "b", "c"]) def test_does_not_error_on_unhashable_kwarg(): with pytest.raises(InvalidArgument): st.builds(lambda alphabet: 1, alphabet=["a", "b", "c"]).validate() def test_caches_floats_sensitively(): assert st.floats(min_value=0.0) is st.floats(min_value=0.0) assert st.floats(min_value=0.0) is not st.floats(min_value=0) assert st.floats(min_value=0.0) is not st.floats(min_value=-0.0) ================================================ FILE: hypothesis-python/tests/cover/test_cathetus.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys from sys import float_info import pytest from hypothesis import assume, given from hypothesis.internal.cathetus import cathetus from hypothesis.strategies import floats def test_cathetus_subnormal_underflow(): u = sys.float_info.min * sys.float_info.epsilon h = 5 * u a = 4 * u assert cathetus(h, a) == 3 * u def test_cathetus_simple_underflow(): a = sys.float_info.min h = a * math.sqrt(2) b = cathetus(h, a) assert b > 0, f"expecting positive cathetus({h:g}, {a:g}), got {b:g}" def test_cathetus_huge_no_overflow(): h = sys.float_info.max a = h / math.sqrt(2) b = cathetus(h, a) assert math.isfinite(b), f"expecting finite cathetus({h:g}, {a:g}), got {b:g}" def test_cathetus_large_no_overflow(): h = sys.float_info.max / 3 a = h / math.sqrt(2) b = cathetus(h, a) assert math.isfinite(b), f"expecting finite cathetus({h:g}, {a:g}), got {b:g}" @pytest.mark.parametrize( "h,a", [ # NaN hypot (math.nan, 3), (math.nan, 0), (math.nan, math.inf), (math.nan, math.nan), # Infeasible (2, 3), (2, -3), (2, math.inf), (2, math.nan), # Surprisingly consistent with c99 hypot() (math.inf, math.inf), ], ) def test_cathetus_nan(h, a): assert math.isnan(cathetus(h, a)) @pytest.mark.parametrize( "h,a", [(math.inf, 3), (math.inf, -3), (math.inf, 0), (math.inf, math.nan)] ) def test_cathetus_infinite(h, a): assert math.isinf(cathetus(h, a)) @pytest.mark.parametrize( "h,a,b", [(-5, 4, 3), (5, -4, 3), (-5, -4, 3), (0, 0, 0), (1, 0, 1)] ) def test_cathetus_signs(h, a, b): assert abs(cathetus(h, a) - b) <= abs(b) * float_info.epsilon @given( h=floats(0) | floats(min_value=1e308, allow_infinity=False), a=floats(0, allow_infinity=False) | floats(min_value=0, max_value=1e250, allow_infinity=False), ) def test_cathetus_always_leq_hypot(h, a): assume(h >= a) b = cathetus(h, a) assert 0 <= b <= h @pytest.mark.parametrize( "a,b,h", [ (3, 4, 5), (5, 12, 13), (8, 15, 17), (7, 24, 25), (20, 21, 29), (12, 35, 37), (9, 40, 41), (28, 45, 53), (11, 60, 61), (16, 63, 65), (33, 56, 65), (48, 55, 73), (13, 84, 85), (36, 77, 85), (39, 80, 89), (65, 72, 97), (20, 99, 101), (60, 91, 109), (15, 112, 113), (44, 117, 125), (88, 105, 137), (17, 144, 145), (24, 143, 145), (51, 140, 149), (85, 132, 157), (119, 120, 169), (52, 165, 173), (19, 180, 181), (57, 176, 185), (104, 153, 185), (95, 168, 193), (28, 195, 197), (84, 187, 205), (133, 156, 205), (21, 220, 221), (140, 171, 221), (60, 221, 229), (105, 208, 233), (120, 209, 241), (32, 255, 257), (23, 264, 265), (96, 247, 265), (69, 260, 269), (115, 252, 277), (160, 231, 281), (161, 240, 289), (68, 285, 293), ], ) def test_pythagorean_triples(a, b, h): assert abs(math.hypot(a, b) - h) <= abs(h) * float_info.epsilon assert abs(cathetus(h, a) - b) <= abs(b) * float_info.epsilon ================================================ FILE: hypothesis-python/tests/cover/test_charmap.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import sys import tempfile import time import unicodedata from typing import get_args import pytest from hypothesis import given, strategies as st from hypothesis.internal import charmap as cm from hypothesis.internal.intervalsets import IntervalSet from tests.common.utils import skipif_threading def test_charmap_contains_all_unicode(): n = 0 for vs in cm.charmap().values(): for u, v in vs: n += v - u + 1 assert n == sys.maxunicode + 1 def test_charmap_has_right_categories(): for cat, intervals in cm.charmap().items(): for u, v in intervals: for i in range(u, v + 1): real = unicodedata.category(chr(i)) assert real == cat, f"{i} is {real} but reported in {cat}" def assert_valid_range_list(ls): for u, v in ls: assert u <= v for i in range(len(ls) - 1): assert ls[i] <= ls[i + 1] assert ls[i][-1] < ls[i + 1][0] @given(st.sets(st.sampled_from(cm.categories()))) def test_query_matches_categories(cats): values = cm.query(categories=cats).intervals assert_valid_range_list(values) for u, v in values: for i in (u, v, (u + v) // 2): assert unicodedata.category(chr(i)) in cats @given( st.sets(st.sampled_from(cm.categories())) | st.none(), st.integers(0, sys.maxunicode), st.integers(0, sys.maxunicode), ) def test_query_matches_categories_codepoints(cats, m1, m2): m1, m2 = sorted((m1, m2)) values = cm.query(categories=cats, min_codepoint=m1, max_codepoint=m2).intervals assert_valid_range_list(values) for u, v in values: assert m1 <= u assert v <= m2 # any test which sets `_charmap = None` is thread-unsafe. @skipif_threading def test_reload_charmap(): x = cm.charmap() assert x is cm.charmap() cm._charmap = None y = cm.charmap() assert x is not y assert x == y @skipif_threading def test_recreate_charmap(): x = cm.charmap() assert x is cm.charmap() cm._charmap = None cm.charmap_file().unlink() y = cm.charmap() assert x is not y assert x == y # This test fails flakily (every 1 in ~10 full CI runs), but only on the test-pyodide # ci job: # # os.utime(cm.charmap_file(), (mtime, mtime)) # ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # FileNotFoundError: [Errno 44] No such file or directory: # '/home/runner/work/hypothesis/hypothesis/.hypothesis/unicode_data/15.1.0/charmap.json.gz' # # I suspect this is from a race condition due to how we parallelize the pyodide # tests in CI, (splitting test files across 20 processes). It's also possible # it's from some pyodide-specific weirdness, but since the test only fails sometimes, # I find this less likely. # # I'm xfailing this on emscripten for now, to get a consistent green CI. @pytest.mark.xfail( condition=sys.platform == "emscripten", strict=False, reason="see comment" ) @skipif_threading def test_uses_cached_charmap(): cm.charmap() # Reset the last-modified time of the cache file to a point in the past. mtime = int(time.time() - 1000) os.utime(cm.charmap_file(), (mtime, mtime)) statinfo = cm.charmap_file().stat() assert statinfo.st_mtime == mtime # Force reload of charmap from cache file and check that mtime is unchanged. cm._charmap = None cm.charmap() statinfo = cm.charmap_file().stat() assert statinfo.st_mtime == mtime def _union_intervals(x, y): return IntervalSet(x).union(IntervalSet(y)).intervals def test_union_empty(): assert _union_intervals([], []) == () assert _union_intervals([], [[1, 2]]) == ((1, 2),) assert _union_intervals([[1, 2]], []) == ((1, 2),) def test_union_handles_totally_overlapped_gap(): # < xx > Imagine the intervals x and y as bit strings. # | The bit at position n is set if n falls inside that interval. # = In this model _union_intervals() performs bit-wise or. assert _union_intervals([[2, 3]], [[1, 2], [4, 5]]) == ((1, 5),) def test_union_handles_partially_overlapped_gap(): # < x > Imagine the intervals x and y as bit strings. # | The bit at position n is set if n falls inside that interval. # = In this model _union_intervals() performs bit-wise or. assert _union_intervals([[3, 3]], [[1, 2], [5, 5]]) == ((1, 3), (5, 5)) def test_successive_union(): x = [] for v in cm.charmap().values(): x = _union_intervals(x, v) assert x == ((0, sys.maxunicode),) @skipif_threading def test_can_handle_race_between_exist_and_create(monkeypatch): x = cm.charmap() cm._charmap = None monkeypatch.setattr(os.path, "exists", lambda p: False) y = cm.charmap() assert x is not y assert x == y @skipif_threading def test_exception_in_write_does_not_lead_to_broken_charmap(monkeypatch): def broken(*args, **kwargs): raise ValueError cm._charmap = None monkeypatch.setattr(os.path, "exists", lambda p: False) monkeypatch.setattr(os, "rename", broken) cm.charmap() cm.charmap() @skipif_threading def test_regenerate_broken_charmap_file(): cm.charmap() cm.charmap_file().write_bytes(b"") # overwrite with empty file cm._charmap = None cm.charmap() def test_exclude_characters_are_included_in_key(): assert cm.query().intervals != cm.query(exclude_characters="0").intervals @skipif_threading def test_error_writing_charmap_file_is_suppressed(monkeypatch): def broken_mkstemp(dir): raise RuntimeError monkeypatch.setattr(tempfile, "mkstemp", broken_mkstemp) try: # Cache the charmap to avoid a performance hit the next time # somebody tries to use it. saved = cm._charmap cm._charmap = None cm.charmap_file().unlink() cm.charmap() finally: cm._charmap = saved def test_categoryname_literal_is_correct(): minor_categories = set(cm.categories()) major_categories = {c[0] for c in minor_categories} assert set(get_args(cm.CategoryName)) == minor_categories | major_categories ================================================ FILE: hypothesis-python/tests/cover/test_compat.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from collections import defaultdict, namedtuple from dataclasses import dataclass from functools import partial from inspect import Parameter, Signature, signature from typing import ForwardRef, Union import pytest from hypothesis.internal.compat import ( add_note, ceil, dataclass_asdict, extract_bits, floor, get_type_hints, ) floor_ceil_values = [ -10.7, -10.3, -0.5, -0.0, 0, 0.5, 10.3, 10.7, ] @pytest.mark.parametrize("value", floor_ceil_values) def test_our_floor_agrees_with_math_floor(value): assert floor(value) == math.floor(value) @pytest.mark.parametrize("value", floor_ceil_values) def test_our_ceil_agrees_with_math_ceil(value): assert ceil(value) == math.ceil(value) class WeirdSig: __signature__ = Signature( parameters=[Parameter(name="args", kind=Parameter.VAR_POSITIONAL)] ) def test_no_type_hints(): assert get_type_hints(WeirdSig) == {} @dataclass class Foo: x: "Foo" = None # type: ignore Foo.__signature__ = signature(Foo).replace( # type: ignore parameters=[ Parameter( "x", Parameter.POSITIONAL_OR_KEYWORD, annotation=ForwardRef("Foo"), default=None, ) ] ) @dataclass class Bar: x: Union[int, "Bar"] | None Bar.__signature__ = signature(Bar).replace( # type: ignore parameters=[ Parameter( "x", Parameter.POSITIONAL_OR_KEYWORD, # ruff reports a false-positive UP007 here, since int | ForwardRef("Bar") # errors on 3.10. We can change this once we drop 3.10. # # see also https://github.com/astral-sh/ruff/issues/20883 annotation=Union[int, ForwardRef("Bar"), None], # type: ignore # noqa: UP007 ) ] ) @pytest.mark.parametrize("obj,expected", [(Foo, Foo | None), (Bar, int | Bar | None)]) def test_resolve_fwd_refs(obj, expected): # See: https://github.com/HypothesisWorks/hypothesis/issues/3519 assert get_type_hints(obj)["x"] == expected def func(a, b: int, *c: str, d: int | None = None): pass @pytest.mark.parametrize( "pf, names", [ (partial(func, 1), "b c d"), (partial(func, 1, 2), "c d"), (partial(func, 1, 2, 3, 4, 5), "c d"), # varargs don't fill (partial(func, 1, 2, 3, d=4), "c d"), # kwonly args just get new defaults ], ) def test_get_hints_through_partial(pf, names): assert set(get_type_hints(pf)) == set(names.split()) @dataclass class FilledWithStuff: a: list b: tuple c: namedtuple d: dict e: defaultdict def test_dataclass_asdict(): ANamedTuple = namedtuple("ANamedTuple", ("with_some_field")) e = defaultdict(list) e["a"].append(1) obj = FilledWithStuff(a=[1], b=(2), c=ANamedTuple(3), d={4: 5}, e=e) assert dataclass_asdict(obj) == { "a": [1], "b": (2), "c": ANamedTuple(3), "d": {4: 5}, "e": {"a": [1]}, } @pytest.mark.parametrize("width", [None, 8]) @pytest.mark.parametrize("x", [0, 2, 123]) def test_extract_bits_roundtrip(width, x): bits = extract_bits(x, width=width) if width is not None: assert len(bits) == width assert x == sum(v << p for p, v in enumerate(reversed(bits))) @dataclass(frozen=True, slots=False) class ImmutableError: msg: str def test_add_note_fails_gracefully_on_frozen_instance(): add_note(ImmutableError("msg"), "some note") ================================================ FILE: hypothesis-python/tests/cover/test_complex_numbers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys import pytest from hypothesis import HealthCheck, given, reject, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.strategies import complex_numbers from tests.common.debug import ( assert_no_examples, check_can_generate_examples, find_any, minimal, ) def test_minimal(): assert minimal(complex_numbers()) == 0 def test_minimal_nonzero_real(): assert minimal(complex_numbers(), lambda x: x.real != 0) == 1 def test_minimal_nonzero_imaginary(): assert minimal(complex_numbers(), lambda x: x.imag != 0) == 1j def test_minimal_quadrant1(): assert minimal(complex_numbers(), lambda x: x.imag > 0 and x.real > 0) == 1 + 1j def test_minimal_quadrant2(): assert minimal(complex_numbers(), lambda x: x.imag > 0 and x.real < 0) == -1 + 1j def test_minimal_quadrant3(): assert minimal(complex_numbers(), lambda x: x.imag < 0 and x.real < 0) == -1 - 1j def test_minimal_quadrant4(): assert minimal(complex_numbers(), lambda x: x.imag < 0 and x.real > 0) == 1 - 1j @given(st.data(), st.integers(-5, 5).map(lambda x: 10**x)) def test_max_magnitude_respected(data, mag): c = data.draw(complex_numbers(max_magnitude=mag)) # Note we accept epsilon errors here as internally sqrt is used to draw # complex numbers. sqrt on some platforms gets epsilon errors, which is # too tricky to filter out and so - for now - we just accept them. assert abs(c) <= mag * (1 + sys.float_info.epsilon) @given(complex_numbers(max_magnitude=0)) def test_max_magnitude_zero(val): assert val == 0 @settings(suppress_health_check=[HealthCheck.too_slow]) @given(st.data(), st.integers(-5, 5).map(lambda x: 10**x)) def test_min_magnitude_respected(data, mag): c = data.draw(complex_numbers(min_magnitude=mag)) # See test_max_magnitude_respected comment assert ( abs(c.real) >= mag or abs(c.imag) >= mag or abs(c) >= mag * (1 - sys.float_info.epsilon) ) def test_minimal_min_magnitude_zero(): assert minimal(complex_numbers(min_magnitude=0)) == 0 def test_minimal_min_magnitude_positive(): assert minimal(complex_numbers(min_magnitude=0.5)) in (0.5, 1) def test_minimal_minmax_magnitude(): assert minimal(complex_numbers(min_magnitude=0.5, max_magnitude=1.5)) in (0.5, 1) @given(st.data(), st.floats(0, 10e300, allow_infinity=False, allow_nan=False)) def test_minmax_magnitude_equal(data, mag): val = data.draw(st.complex_numbers(min_magnitude=mag, max_magnitude=mag)) try: # Cap magnitude at 10e300 to avoid float overflow, and imprecision # at very large exponents (which makes math.isclose fail) assert math.isclose(abs(val), mag) except OverflowError: reject() def _is_subnormal(x): return 0 < abs(x) < sys.float_info.min @pytest.mark.parametrize( "allow_subnormal, min_magnitude, max_magnitude", [ (True, 0, None), (True, 1, None), (False, 0, None), ], ) def test_allow_subnormal(allow_subnormal, min_magnitude, max_magnitude): strat = complex_numbers( min_magnitude=min_magnitude, max_magnitude=max_magnitude, allow_subnormal=allow_subnormal, ).filter(lambda x: x.real != 0 and x.imag != 0) if allow_subnormal: find_any(strat, lambda x: _is_subnormal(x.real) or _is_subnormal(x.imag)) else: assert_no_examples( strat, lambda x: _is_subnormal(x.real) or _is_subnormal(x.imag) ) @pytest.mark.parametrize("allow_subnormal", [1, 0.0, "False"]) def test_allow_subnormal_validation(allow_subnormal): with pytest.raises(InvalidArgument): check_can_generate_examples(complex_numbers(allow_subnormal=allow_subnormal)) ================================================ FILE: hypothesis-python/tests/cover/test_composite.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import typing import pytest from hypothesis import HealthCheck, assume, given, settings, strategies as st from hypothesis.errors import ( HypothesisDeprecationWarning, HypothesisWarning, InvalidArgument, ) from tests.common.debug import minimal from tests.common.utils import flaky @st.composite def badly_draw_lists(draw, m=0): length = draw(st.integers(m, m + 10)) return [draw(st.integers()) for _ in range(length)] def test_simplify_draws(): assert minimal(badly_draw_lists(), lambda x: len(x) >= 3) == [0] * 3 def test_can_pass_through_arguments(): assert minimal(badly_draw_lists(5)) == [0] * 5 assert minimal(badly_draw_lists(m=6)) == [0] * 6 @st.composite def draw_ordered_with_assume(draw): x = draw(st.floats()) y = draw(st.floats()) assume(x < y) return (x, y) @given(draw_ordered_with_assume()) @settings(suppress_health_check=[HealthCheck.filter_too_much]) def test_can_assume_in_draw(xy): assert xy[0] < xy[1] def test_uses_definitions_for_reprs(): assert repr(badly_draw_lists()) == "badly_draw_lists()" assert repr(badly_draw_lists(1)) == "badly_draw_lists(m=1)" assert repr(badly_draw_lists(m=1)) == "badly_draw_lists(m=1)" def test_errors_given_default_for_draw(): with pytest.raises(InvalidArgument): @st.composite def foo(x=None): pass def test_errors_given_function_of_no_arguments(): with pytest.raises(InvalidArgument): @st.composite def foo(): pass def test_errors_given_kwargs_only(): with pytest.raises(InvalidArgument): @st.composite def foo(**kwargs): pass def test_warning_given_no_drawfn_call(): with pytest.warns(HypothesisDeprecationWarning): @st.composite def foo(_): return "bar" def test_can_use_pure_args(): @st.composite def stuff(*args): return args[0](st.sampled_from(args[1:])) assert minimal(stuff(1, 2, 3, 4, 5)) == 1 def test_composite_of_lists(): @st.composite def f(draw): return draw(st.integers()) + draw(st.integers()) assert minimal(st.lists(f()), lambda x: len(x) >= 10) == [0] * 10 @flaky(min_passes=2, max_runs=5) def test_can_shrink_matrices_with_length_param(): @st.composite def matrix(draw): rows = draw(st.integers(1, 10)) columns = draw(st.integers(1, 10)) return [ [draw(st.integers(0, 10000)) for _ in range(columns)] for _ in range(rows) ] def transpose(m): return [[row[i] for row in m] for i in range(len(m[0]))] def is_square(m): return len(m) == len(m[0]) value = minimal(matrix(), lambda m: is_square(m) and transpose(m) != m) assert len(value) == 2 assert len(value[0]) == 2 assert sorted(value[0] + value[1]) == [0, 0, 0, 1] class MyList(list): pass @given(st.data(), st.lists(st.integers()).map(MyList)) def test_does_not_change_arguments(data, ls): # regression test for issue #1017 or other argument mutation @st.composite def strat(draw, arg): draw(st.none()) return arg ex = data.draw(strat(ls)) assert ex is ls class ClsWithStrategyMethods: @classmethod @st.composite def st_classmethod_then_composite(draw, cls): return draw(st.integers(0, 10)) @st.composite @classmethod def st_composite_then_classmethod(draw, cls): return draw(st.integers(0, 10)) @staticmethod @st.composite def st_staticmethod_then_composite(draw): return draw(st.integers(0, 10)) @st.composite @staticmethod def st_composite_then_staticmethod(draw): return draw(st.integers(0, 10)) @st.composite def st_composite_method(draw, self): return draw(st.integers(0, 10)) @given(st.data()) def test_applying_composite_decorator_to_methods(data): instance = ClsWithStrategyMethods() for strategy in [ ClsWithStrategyMethods.st_classmethod_then_composite(), ClsWithStrategyMethods.st_composite_then_classmethod(), ClsWithStrategyMethods.st_staticmethod_then_composite(), ClsWithStrategyMethods.st_composite_then_staticmethod(), instance.st_classmethod_then_composite(), instance.st_composite_then_classmethod(), instance.st_staticmethod_then_composite(), instance.st_composite_then_staticmethod(), instance.st_composite_method(), ]: x = data.draw(strategy) assert isinstance(x, int) assert 0 <= x <= 10 def test_drawfn_cannot_be_instantiated(): with pytest.raises(TypeError): st.DrawFn() def test_warns_on_strategy_annotation(): with pytest.warns(HypothesisWarning, match="Return-type annotation") as w: @st.composite def my_integers(draw: st.DrawFn) -> st.SearchStrategy[int]: return draw(st.integers()) assert len(w.list) == 1 assert w.list[0].filename == __file__ # check stacklevel points to user code def test_composite_allows_overload_without_draw(): # See https://github.com/HypothesisWorks/hypothesis/issues/3970 @st.composite @typing.overload def overloaded(draw: st.DrawFn, *, x: int) -> typing.Literal[True]: ... @st.composite @typing.overload def overloaded(draw: st.DrawFn, *, x: str) -> typing.Literal[False]: ... @st.composite def overloaded(draw: st.DrawFn, *, x: int | str) -> bool: return draw(st.just(isinstance(x, int))) ================================================ FILE: hypothesis-python/tests/cover/test_composite_kwonlyargs.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st # Tests that convert_keyword_arguments in reflection.py can handle # composites that only have keyword-only arguments. # See https://github.com/HypothesisWorks/hypothesis/issues/1999 @st.composite def kwonlyargs_composites(draw, *, kwarg1=None): return draw(st.fixed_dictionaries({"kwarg1": st.just(kwarg1), "i": st.integers()})) @given( st.lists( st.one_of(kwonlyargs_composites(kwarg1="test")), unique_by=lambda x: x["i"] ) ) def test_composite_with_keyword_only_args(a): pass ================================================ FILE: hypothesis-python/tests/cover/test_constants_ast.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import ast import inspect import subprocess import sys import textwrap from types import ModuleType import pytest from hypothesis import example, given, note, settings, strategies as st from hypothesis.internal.compat import PYPY from hypothesis.internal.constants_ast import ( Constants, ConstantVisitor, TooManyConstants, _constants_file_str, _constants_from_source, constants_from_module, is_local_module_file, ) from tests.common.utils import skipif_emscripten, skipif_threading constant_ints = st.integers(max_value=-101) | st.integers(min_value=101) constant_floats = st.floats(allow_nan=False, allow_infinity=False) constant_bytes = st.binary(min_size=1, max_size=50) constant_strings = st.text(min_size=1, max_size=10).filter(lambda s: not s.isspace()) constants = constant_ints | constant_floats | constant_bytes | constant_strings constants_classes = st.builds( Constants, integers=st.sets(constant_ints), floats=st.sets(constant_floats), bytes=st.sets(constant_bytes), strings=st.sets(constant_strings), ) def constants_from_ast(tree): visitor = ConstantVisitor(limit=True) visitor.visit(tree) return visitor.constants def test_constants_set_from_type_invalid(): with pytest.raises(ValueError): Constants().set_for_type("not_a_type") @given(st.integers()) def test_constants_contains(n): assert n in Constants(integers={n}) @given(constants_classes) def test_constants_not_equal_to_set(constants): assert constants != set() assert constants != set(constants) @pytest.mark.parametrize( "source, expected", [ ( """ a1 = 142 a2 = 3.14 a3 = 'test1' a4 = b'test2' a5 = (101, 102) a6 = frozenset([103]) """, {142, 3.14, 101, 102, 103, "test1", b"test2"}, ), ( "a = (101, (102, 103), frozenset([104, 105]))", {101, 102, 103, 104, 105}, ), ("a = {'b': 101}", {"b", 101}), ("a = [101]", {101}), ("a = +142", {142}), ("a = 101 + 102", {101, 102}), ("a = ~ 142", {142}), # the following cases are ignored: # * booleans # * math.inf and math.nan (not constants, but we don't want to collect them # even if they were) # * f-strings # * long strings # * pure-whitespace strings # * standalone string expressions (strings not assigned to a variable). # This covers docstrings of all kinds. # * small integers # * the empty bytestring b"" ("a = True", set()), ("a = False", set()), ("a = not False", set()), ("a = 1e999", set()), ("a = math.inf", set()), ("a = math.nan", set()), ('a = f"test {x}"', set()), (f'a = "{"b" * 100}"', set()), ('a = ""', set()), ('a = " "', set()), ('a = "\\n \\n \\n"', set()), ("'test'", set()), ("'test with \\n newlines'", set()), ("a = 10", set()), ("a = -1", set()), ("a = b''", set()), ], ) def test_constants_from_ast(source, expected): source = textwrap.dedent(source) tree = ast.parse(source) assert set(constants_from_ast(tree)) == expected @given(st.integers(max_value=-101)) def test_parses_negatives(n): assert constants_from_ast(ast.parse(f"a = {n}")) == Constants(integers={n}) @given(st.tuples(constants)) def test_tuple_constants(value): tree = ast.parse(str(value)) assert set(constants_from_ast(tree)) == set(value) @given(st.frozensets(constants)) def test_frozenset_constants(value): tree = ast.parse(str(value)) assert set(constants_from_ast(tree)) == set(value) @skipif_threading @skipif_emscripten @pytest.mark.xfail( condition=settings.get_current_profile_name() != "ci", strict=False, reason="Requires clean environment", ) def test_constants_from_running_file(tmp_path): p = tmp_path / "my_constants.py" p.write_text( textwrap.dedent( """ import sys # stdlib import json # third-party import pytest import hypothesis from hypothesis.internal.constants_ast import ( is_local_module_file, constants_from_module, Constants ) # these modules are in fact detected as local if they are installed # as editable (as is common for contributors). Prevent the ast constant # logic from picking up on them for module in sys.modules.copy(): if module.startswith("hypofuzz"): del sys.modules[module] constants = Constants() for module in sys.modules.values(): if getattr(module, "__file__", None) is not None and is_local_module_file( module.__file__ ): constants |= constants_from_module(module) expected = Constants( strings={'float', 'string', 'bytes', 'integer', 'test', 'hypofuzz', '__file__'}, floats={3.14}, bytes={b'test'}, integers={142, 101, 102, 103, 104}, ) assert constants == expected, set(constants).symmetric_difference(set(expected)) # local a = 142 b = "test" c = True d = 3.14 e = b"test" f = (101, 102) g = frozenset([103, 104]) """, ), encoding="utf-8", ) # this test doubles as a regression test for # https://github.com/HypothesisWorks/hypothesis/issues/4375. Fail on comparisons # between bytes and str. subprocess.check_call([sys.executable, "-bb", str(p)]) def test_constants_from_bad_module(): # covering test for the except branch module = ModuleType("nonexistent") assert constants_from_module(module) == Constants() @pytest.mark.parametrize( "path", [ "/path/to/tests/module", "/path/to/test/module", "/a/test_file.py", "/a/file_test.py", ], ) def test_local_modules_ignores_test_modules(path): assert not is_local_module_file(path) @skipif_threading @pytest.mark.skipif(PYPY, reason="no memory error on pypy") def test_ignores_ast_parse_error(tmp_path): p = tmp_path / "errors_on_parse.py" p.write_text("[1, " * 200 + "]" * 200, encoding="utf-8") module = ModuleType("") module.__file__ = str(p) source = inspect.getsource(module) with pytest.raises(MemoryError): ast.parse(source) assert constants_from_module(module) == Constants() @example( constants=Constants( integers={-101, 101}, floats={101.0}, bytes=set(), strings=set(), ), ) @example( constants=Constants( integers={-101, 100}, floats={-101.0, 101.0}, bytes={b"-101.0", b"101.0"}, strings={"-101.0", "101.0"}, ), ) @given(constants_classes) def test_constant_visitor_roundtrips_string(constants): # our files in storage_directory("constants") rely on this roundtrip visitor = ConstantVisitor(limit=True) s = _constants_file_str(constants) visitor.visit(ast.parse(s)) note(f"{constants=}") note(f"{visitor.constants=}") assert visitor.constants == constants def test_too_many_constants(): visitor = ConstantVisitor(limit=True) # start at n=1000 to avoid ConstantVisitor ignoring small integers s = "; ".join( f"n = {i}" for i in range(1000, 1000 + ConstantVisitor.CONSTANTS_LIMIT + 1) ) # visitor should raise on too many constants with pytest.raises(TooManyConstants): visitor.visit(ast.parse(s)) # and also _constants_from_source should return empty on too many constants assert _constants_from_source(s, limit=True) == Constants() # but it parses fine with limit=False visitor = ConstantVisitor(limit=False) visitor.visit(ast.parse(s)) assert _constants_from_source(s, limit=False) == Constants( integers=set(range(1000, 1000 + ConstantVisitor.CONSTANTS_LIMIT + 1)) ) @skipif_threading # concurrent writes to the same file def test_module_too_large(tmp_path): constant = 11231783 p = tmp_path / "large_file.py" content = f"a = {constant}\n\n" + "#" * (512 * 1024 + 1) p.write_text(content, encoding="utf-8") module = ModuleType("large_module") module.__file__ = str(p) assert constants_from_module(module) == Constants() assert constants_from_module(module, limit=False) == Constants(integers={constant}) ================================================ FILE: hypothesis-python/tests/cover/test_control.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import dataclass import pytest from hypothesis import Verbosity, assume, given, reject, reporting, settings from hypothesis.control import ( BuildContext, _current_build_context, _event_to_string, cleanup, current_build_context, currently_in_test_context, event, note, ) from hypothesis.errors import ( HypothesisDeprecationWarning, InvalidArgument, UnsatisfiedAssumption, ) from hypothesis.internal.compat import ExceptionGroup from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.stateful import RuleBasedStateMachine, rule from hypothesis.strategies import integers from tests.common.utils import capture_out def bc(): return BuildContext(ConjectureData.for_choices([]), wrapped_test=None) def test_cannot_cleanup_with_no_context(): with pytest.raises(InvalidArgument): cleanup(lambda: None) assert _current_build_context.value is None def test_cannot_event_with_no_context(): with pytest.raises(InvalidArgument): event("hi") assert _current_build_context.value is None def test_cleanup_executes_on_leaving_build_context(): data = [] with bc(): cleanup(lambda: data.append(1)) assert not data assert data == [1] assert _current_build_context.value is None def test_can_nest_build_context(): data = [] with bc(): cleanup(lambda: data.append(1)) with bc(): cleanup(lambda: data.append(2)) assert not data assert data == [2] assert data == [2, 1] assert _current_build_context.value is None def test_does_not_suppress_exceptions(): with pytest.raises(AssertionError), bc(): raise AssertionError assert _current_build_context.value is None def test_suppresses_exceptions_in_teardown(): with pytest.raises(ValueError) as exc_info, bc(): def foo(): raise ValueError cleanup(foo) raise AssertionError assert isinstance(exc_info.value, ValueError) assert isinstance(exc_info.value.__cause__, AssertionError) def test_runs_multiple_cleanup_with_teardown(): with pytest.raises(ExceptionGroup) as exc_info, bc(): def foo(): raise ValueError def bar(): raise TypeError cleanup(foo) cleanup(bar) raise AssertionError assert isinstance(exc_info.value, ExceptionGroup) assert isinstance(exc_info.value.__cause__, AssertionError) assert {type(e) for e in exc_info.value.exceptions} == {ValueError, TypeError} assert _current_build_context.value is None def test_raises_error_if_cleanup_fails_but_block_does_not(): with pytest.raises(ValueError), bc(): def foo(): raise ValueError cleanup(foo) assert _current_build_context.value is None def test_raises_if_note_out_of_context(): with pytest.raises(InvalidArgument): note("Hi") def test_deprecation_warning_if_assume_out_of_context(): with pytest.warns( HypothesisDeprecationWarning, match="Using `assume` outside a property-based test is deprecated", ): assume(True) def test_deprecation_warning_if_reject_out_of_context(): with ( pytest.warns( HypothesisDeprecationWarning, match="Using `reject` outside a property-based test is deprecated", ), pytest.raises(UnsatisfiedAssumption), ): reject() def test_raises_if_current_build_context_out_of_context(): with pytest.raises(InvalidArgument): current_build_context() def test_current_build_context_is_current(): with bc() as a: assert current_build_context() is a def test_prints_all_notes_in_verbose_mode(): # slightly roundabout because @example messes with verbosity - see #1521 messages = set() @settings(verbosity=Verbosity.debug, database=None) @given(integers(1, 10)) def test(x): msg = f"x -> {x}" note(msg) messages.add(msg) assert x < 5 with ( capture_out() as out, reporting.with_reporter(reporting.default), pytest.raises(AssertionError), ): test() v = out.getvalue() for x in sorted(messages): assert x in v @dataclass class CanBePrettyPrinted: n: int def test_note_pretty_prints(): reports = [] @given(integers(1, 10)) def test(n): with reporting.with_reporter(reports.append): note(CanBePrettyPrinted(n)) assert n != 5 with pytest.raises(AssertionError): test() assert reports == ["CanBePrettyPrinted(n=5)"] def test_not_currently_in_hypothesis(): assert currently_in_test_context() is False @given(integers()) def test_currently_in_hypothesis(_): assert currently_in_test_context() is True class ContextMachine(RuleBasedStateMachine): @rule() def step(self): assert currently_in_test_context() is True test_currently_in_stateful_test = ContextMachine.TestCase def test_can_convert_non_weakref_types_to_event_strings(): _event_to_string((), avoid_realization=True) _event_to_string((), avoid_realization=False) ================================================ FILE: hypothesis-python/tests/cover/test_core.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import unittest import pytest from _pytest.outcomes import Failed, Skipped from hypothesis import Phase, example, find, given, reject, settings, strategies as st from hypothesis.database import InMemoryExampleDatabase from hypothesis.errors import InvalidArgument, NoSuchExample, Unsatisfiable def test_stops_after_max_examples_if_satisfying(): count = 0 def track(x): nonlocal count count += 1 return False max_examples = 100 with pytest.raises(NoSuchExample): find(st.integers(0, 10000), track, settings=settings(max_examples=max_examples)) assert count == max_examples def test_stops_after_ten_times_max_examples_if_not_satisfying(): count = 0 def track(x): nonlocal count count += 1 reject() max_examples = 100 with pytest.raises(Unsatisfiable): find(st.integers(0, 10000), track, settings=settings(max_examples=max_examples)) # Very occasionally we can generate overflows in generation, which also # count towards our example budget, which means that we don't hit the # maximum. assert count <= 10 * max_examples some_normal_settings = settings() def test_is_not_normally_default(): assert settings.default is not some_normal_settings @given(st.booleans()) @some_normal_settings def test_settings_are_default_in_given(x): assert settings.default is some_normal_settings def test_given_shrinks_pytest_helper_errors(): final_value = None @settings(derandomize=True, max_examples=100) @given(st.integers()) def inner(x): nonlocal final_value final_value = x if x > 100: pytest.fail(f"{x=} is too big!") with pytest.raises(Failed): inner() assert final_value == 101 def test_pytest_skip_skips_shrinking(): seen_large = False @settings(derandomize=True, max_examples=100) @given(st.integers()) def inner(x): nonlocal seen_large if x > 100: if seen_large: raise Exception("Should never replay a skipped test!") seen_large = True pytest.skip(f"{x=} is too big!") with pytest.raises(Skipped): inner() def test_can_find_with_db_eq_none(): find(st.integers(), bool, settings=settings(database=None, max_examples=100)) def test_no_such_example(): with pytest.raises(NoSuchExample): find(st.none(), bool, database_key=b"no such example") def test_validates_strategies_for_test_method(): invalid_strategy = st.lists(st.nothing(), min_size=1) class TestStrategyValidation: @given(invalid_strategy) def test_method_with_bad_strategy(self, x): pass instance = TestStrategyValidation() with pytest.raises(InvalidArgument): instance.test_method_with_bad_strategy() @example(1) @given(st.integers()) @settings(phases=[Phase.target, Phase.shrink, Phase.explain]) def no_phases(_): raise Exception @given(st.integers()) @settings(phases=[Phase.explicit]) def no_explicit(_): raise Exception @given(st.integers()) @settings(phases=[Phase.reuse], database=InMemoryExampleDatabase()) def empty_db(_): raise Exception @pytest.mark.parametrize( "test_fn", [no_phases, no_explicit, empty_db], ids=lambda t: t.__name__, ) def test_non_executed_tests_raise_skipped(test_fn): with pytest.raises(unittest.SkipTest): test_fn() @pytest.mark.parametrize( "codec, max_codepoint, exclude_categories, categories", [ ("ascii", None, None, None), ("ascii", 128, None, None), ("ascii", 100, None, None), ("utf-8", None, None, None), ("utf-8", None, ["Cs"], None), ("utf-8", None, ["N"], None), ("utf-8", None, None, ["N"]), ], ) @given(st.data()) def test_characters_codec(codec, max_codepoint, exclude_categories, categories, data): strategy = st.characters( codec=codec, max_codepoint=max_codepoint, exclude_categories=exclude_categories, categories=categories, ) example = data.draw(strategy) assert example.encode(encoding=codec).decode(encoding=codec) == example ================================================ FILE: hypothesis-python/tests/cover/test_custom_reprs.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import pytest from hypothesis import given, settings, strategies as st from hypothesis.strategies._internal.lazy import unwrap_strategies def test_includes_non_default_args_in_repr(): assert repr(st.integers()) == "integers()" assert repr(st.integers(min_value=1)) == "integers(min_value=1)" def test_sampled_repr_leaves_range_as_range(): huge = 10**100 assert repr(st.sampled_from(range(huge))) == f"sampled_from(range(0, {huge}))" def hi(there, stuff): return there def test_supports_positional_and_keyword_args_in_builds(): assert ( repr(st.builds(hi, st.integers(), there=st.booleans())) == "builds(hi, integers(), there=booleans())" ) def test_preserves_sequence_type_of_argument(): assert repr(st.sampled_from([0, 1])) == "sampled_from([0, 1])" assert repr(st.sampled_from((0, 1))) == "sampled_from((0, 1))" class IHaveABadRepr: def __repr__(self): raise ValueError("Oh no!") def test_errors_are_deferred_until_repr_is_calculated(): s = ( st.builds( lambda x, y: 1, st.just(IHaveABadRepr()), y=st.one_of(st.sampled_from((IHaveABadRepr(),)), st.just(IHaveABadRepr())), ) .map(lambda t: t) .filter(lambda t: True) .flatmap(lambda t: st.just(IHaveABadRepr())) ) with pytest.raises(ValueError): repr(s) @given(st.iterables(st.integers())) def test_iterables_repr_is_useful(it): # fairly hard-coded but useful; also ensures _values are inexhaustible assert repr(it) == f"iter({it._values!r})" class Foo: def __init__(self, x: int) -> None: self.x = x class Bar(Foo): pass def test_reprs_as_created(): @given(foo=st.builds(Foo), bar=st.from_type(Bar), baz=st.none().map(Foo)) @settings(print_blob=False, max_examples=10_000, derandomize=True) def inner(foo, bar, baz): assert baz.x is None assert foo.x <= 0 or bar.x >= 0 with pytest.raises(AssertionError) as err: inner() expected = """ Falsifying example: inner( foo=Foo(x=1), bar=Bar(x=-1), baz=Foo(None), ) """ assert "\n".join(err.value.__notes__).strip() == expected.strip() def test_reprs_as_created_interactive(): @given(st.data()) @settings(print_blob=False, max_examples=10_000) def inner(data): bar = data.draw(st.builds(Bar, st.just(10))) assert not bar.x with pytest.raises(AssertionError) as err: inner() expected = """ Falsifying example: inner( data=data(...), ) Draw 1: Bar(10) """ assert "\n".join(err.value.__notes__).strip() == expected.strip() CONSTANT_FOO = Foo(None) def some_foo(*_): return CONSTANT_FOO def test_as_created_reprs_fallback_for_distinct_calls_same_obj(): # If we have *different* calls which return the *same* object, we skip our # nice repr because it's unclear which one we should use. @given(st.builds(some_foo), st.builds(some_foo, st.none())) @settings(print_blob=False, max_examples=10_000) def inner(a, b): assert a is not b with pytest.raises(AssertionError) as err: inner() expected_re = r""" Falsifying example: inner\( a=<.*Foo object at 0x[0-9A-Fa-f]+>, b=<.*Foo object at 0x[0-9A-Fa-f]+>, \) """.strip() got = "\n".join(err.value.__notes__).strip() assert re.fullmatch(expected_re, got), got def test_reprs_as_created_consistent_calls_despite_indentation(): aas = "a" * 60 strat = st.builds(some_foo, st.just(aas)) # If we have multiple calls which return the same object, we can print their # nice repr even if varying indentation means that they'll come out different! @given(strat, st.builds(Bar, strat)) @settings(print_blob=False, max_examples=10_000) def inner(a, b): assert a == b with pytest.raises(AssertionError) as err: inner() expected = f""" Falsifying example: inner( a=some_foo({aas!r}), b=Bar( some_foo( {aas!r}, ), ), ) """ assert "\n".join(err.value.__notes__).strip() == expected.strip() @pytest.mark.parametrize( "strategy, expected_repr", [ (st.characters(), "characters()"), (st.characters(codec="utf-8"), "characters(codec='utf-8')"), (st.characters(min_codepoint=65), "characters(min_codepoint=65)"), (st.characters(max_codepoint=127), "characters(max_codepoint=127)"), (st.characters(categories=["Lu", "Ll"]), "characters(categories=('Lu', 'Ll'))"), ( st.characters(exclude_characters="abc"), "characters(exclude_characters='abc')", ), ( st.characters(min_codepoint=65, max_codepoint=90), "characters(min_codepoint=65, max_codepoint=90)", ), ( st.characters(codec="ascii", min_codepoint=32, max_codepoint=126), "characters(min_codepoint=32, max_codepoint=126)", ), ( st.characters(categories=["Lu"], exclude_characters="AZ"), "characters(categories=('Lu',), exclude_characters='AZ')", ), ], ) def test_characters_repr(strategy, expected_repr): assert repr(unwrap_strategies(strategy)) == expected_repr ================================================ FILE: hypothesis-python/tests/cover/test_database_backend.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import re import shutil import tempfile import time import zipfile from collections import Counter from collections.abc import Iterable, Iterator from contextlib import contextmanager, nullcontext from datetime import datetime, timedelta, timezone from pathlib import Path from shutil import make_archive, rmtree import pytest from hypothesis import ( HealthCheck, configuration, example, given, settings, strategies as st, ) from hypothesis.database import ( BackgroundWriteDatabase, DirectoryBasedExampleDatabase, ExampleDatabase, GitHubArtifactDatabase, InMemoryExampleDatabase, MultiplexedDatabase, ReadOnlyDatabase, _db_for_path, _pack_uleb128, _unpack_uleb128, choices_from_bytes, choices_to_bytes, ) from hypothesis.errors import HypothesisDeprecationWarning, HypothesisWarning from hypothesis.internal.compat import WINDOWS from hypothesis.internal.conjecture.choice import choice_equal from hypothesis.stateful import ( Bundle, RuleBasedStateMachine, invariant, precondition, rule, run_state_machine_as_test, ) from hypothesis.strategies import binary, lists, tuples from hypothesis.utils.conventions import not_set from tests.common.utils import ( checks_deprecated_behaviour, skipif_emscripten, skipif_threading, wait_for, ) from tests.conjecture.common import nodes, nodes_inline # we need real time here, not monkeypatched for CI time_sleep = time.sleep @given(lists(tuples(binary(), binary()))) @settings(max_examples=50) def test_backend_returns_what_you_put_in(xs): backend = InMemoryExampleDatabase() mapping = {} for key, value in xs: mapping.setdefault(key, set()).add(value) backend.save(key, value) for key, values in mapping.items(): backend_contents = list(backend.fetch(key)) distinct_backend_contents = set(backend_contents) assert len(backend_contents) == len(distinct_backend_contents) assert distinct_backend_contents == set(values) def test_can_delete_keys(): backend = InMemoryExampleDatabase() backend.save(b"foo", b"bar") backend.save(b"foo", b"baz") backend.delete(b"foo", b"bar") assert list(backend.fetch(b"foo")) == [b"baz"] def test_default_database_is_in_memory(): with pytest.warns(HypothesisDeprecationWarning): assert isinstance(ExampleDatabase(), InMemoryExampleDatabase) def test_default_on_disk_database_is_dir(tmp_path): with pytest.warns(HypothesisDeprecationWarning): assert isinstance( ExampleDatabase(tmp_path.joinpath("foo")), DirectoryBasedExampleDatabase ) def test_does_not_error_when_fetching_when_not_exist(tmp_path): db = DirectoryBasedExampleDatabase(tmp_path / "examples") db.fetch(b"foo") @pytest.fixture(scope="function", params=["memory", "directory"]) def exampledatabase(request, tmp_path): if request.param == "memory": return InMemoryExampleDatabase() assert request.param == "directory" return DirectoryBasedExampleDatabase(tmp_path / "examples") def test_can_delete_a_key_that_is_not_present(exampledatabase): exampledatabase.delete(b"foo", b"bar") def test_can_fetch_a_key_that_is_not_present(exampledatabase): assert list(exampledatabase.fetch(b"foo")) == [] def test_saving_a_key_twice_fetches_it_once(exampledatabase): exampledatabase.save(b"foo", b"bar") exampledatabase.save(b"foo", b"bar") assert list(exampledatabase.fetch(b"foo")) == [b"bar"] def test_can_close_a_database_after_saving(exampledatabase): exampledatabase.save(b"foo", b"bar") def test_class_name_is_in_repr(exampledatabase): assert type(exampledatabase).__name__ in repr(exampledatabase) def test_an_absent_value_is_present_after_it_moves(exampledatabase): exampledatabase.move(b"a", b"b", b"c") assert next(exampledatabase.fetch(b"b")) == b"c" def test_an_absent_value_is_present_after_it_moves_to_self(exampledatabase): exampledatabase.move(b"a", b"a", b"b") assert next(exampledatabase.fetch(b"a")) == b"b" @skipif_threading def test_two_directory_databases_can_interact(tmp_path): db1 = DirectoryBasedExampleDatabase(tmp_path) db2 = DirectoryBasedExampleDatabase(tmp_path) db1.save(b"foo", b"bar") assert list(db2.fetch(b"foo")) == [b"bar"] db2.save(b"foo", b"bar") db2.save(b"foo", b"baz") assert sorted(db1.fetch(b"foo")) == [b"bar", b"baz"] def test_can_handle_disappearing_files(tmp_path, monkeypatch): db = DirectoryBasedExampleDatabase(tmp_path) db.save(b"foo", b"bar") base_listdir = os.listdir monkeypatch.setattr( os, "listdir", lambda d: [*base_listdir(d), "this-does-not-exist"] ) assert list(db.fetch(b"foo")) == [b"bar"] def test_readonly_db_is_not_writable(): inner = InMemoryExampleDatabase() wrapped = ReadOnlyDatabase(inner) inner.save(b"key", b"value") inner.save(b"key", b"value2") wrapped.delete(b"key", b"value") wrapped.move(b"key", b"key2", b"value2") wrapped.save(b"key", b"value3") assert set(wrapped.fetch(b"key")) == {b"value", b"value2"} assert set(wrapped.fetch(b"key2")) == set() def test_multiplexed_dbs_read_and_write_all(): a = InMemoryExampleDatabase() b = InMemoryExampleDatabase() multi = MultiplexedDatabase(a, b) a.save(b"a", b"aa") b.save(b"b", b"bb") multi.save(b"c", b"cc") multi.move(b"a", b"b", b"aa") for db in (a, b, multi): assert set(db.fetch(b"a")) == set() assert set(db.fetch(b"c")) == {b"cc"} got = list(multi.fetch(b"b")) assert len(got) == 2 assert set(got) == {b"aa", b"bb"} multi.delete(b"c", b"cc") for db in (a, b, multi): assert set(db.fetch(b"c")) == set() def test_ga_require_readonly_wrapping(): """Test that GitHubArtifactDatabase requires wrapping around ReadOnlyDatabase""" database = GitHubArtifactDatabase("test", "test") # save, move and delete can only be called when wrapped around ReadonlyDatabase with pytest.raises(RuntimeError, match=re.escape(database._read_only_message)): database.save(b"foo", b"bar") with pytest.raises(RuntimeError): database.move(b"foo", b"bar", b"foobar") with pytest.raises(RuntimeError): database.delete(b"foo", b"bar") # check that the database silently ignores writes when wrapped around ReadOnlyDatabase database = ReadOnlyDatabase(database) database.save(b"foo", b"bar") database.move(b"foo", b"bar", b"foobar") database.delete(b"foo", b"bar") @contextmanager def ga_empty_artifact( date: datetime | None = None, path: Path | None = None ) -> Iterator[tuple[Path, Path]]: """Creates an empty GitHub artifact.""" if date: timestamp = date.isoformat().replace(":", "_") else: timestamp = datetime.now(timezone.utc).isoformat().replace(":", "_") temp_dir = None if not path: temp_dir = tempfile.mkdtemp() path = Path(temp_dir) / "github-artifacts" path.mkdir(parents=True, exist_ok=True) zip_path = path / f"{timestamp}.zip" with zipfile.ZipFile(zip_path, "w"): pass try: yield (path, zip_path) finally: if temp_dir: rmtree(temp_dir) def test_ga_empty_read(): """Tests that an inexistent key returns nothing.""" with ga_empty_artifact() as (path, _): database = GitHubArtifactDatabase("test", "test", path=path) assert list(database.fetch(b"foo")) == [] def test_ga_initialize(): """ Tests that the database is initialized when a new artifact is found. As well that initialization doesn't happen again on the next fetch. """ now = datetime.now(timezone.utc) with ga_empty_artifact(date=(now - timedelta(hours=2))) as (path, _): database = GitHubArtifactDatabase("test", "test", path=path) # Trigger initialization list(database.fetch(b"")) initial_artifact = database._artifact assert initial_artifact assert database._artifact assert database._access_cache is not None with ga_empty_artifact(date=now, path=path) as (path, _): # Initialization shouldn't happen again list(database.fetch(b"")) assert database._initialized assert database._artifact == initial_artifact def test_ga_no_artifact(tmp_path): """Tests that the database is disabled when no artifact is found.""" database = GitHubArtifactDatabase("test", "test", path=tmp_path) # Check that the database raises a warning with pytest.warns(HypothesisWarning): assert list(database.fetch(b"")) == [] assert database._disabled is True assert list(database.fetch(b"")) == [] def test_ga_corrupted_artifact(): """Tests that corrupted artifacts are properly detected and warned about.""" with ga_empty_artifact() as (path, zip_path): # Corrupt the CRC of the zip file with open(zip_path, "rb+") as f: f.write(b"\x00\x01\x00\x01") database = GitHubArtifactDatabase("test", "test", path=path) # Check that the database raises a warning with pytest.warns(HypothesisWarning): assert list(database.fetch(b"")) == [] assert database._disabled is True def test_ga_deletes_old_artifacts(): """Tests that old artifacts are automatically deleted.""" now = datetime.now(timezone.utc) with ( ga_empty_artifact(date=now) as (path, file_now), ga_empty_artifact(date=now - timedelta(hours=2), path=path) as ( _, file_old, ), ): database = GitHubArtifactDatabase("test", "test", path=path) # Trigger initialization list(database.fetch(b"")) assert file_now.exists() assert not file_old.exists() @skipif_threading def test_ga_triggers_fetching(monkeypatch, tmp_path): """Tests whether an artifact fetch is triggered, and an expired artifact is deleted.""" with ga_empty_artifact() as (_, artifact): # We patch the _fetch_artifact method to return our artifact def fake_fetch_artifact(self) -> Path | None: return artifact monkeypatch.setattr( GitHubArtifactDatabase, "_fetch_artifact", fake_fetch_artifact ) database = GitHubArtifactDatabase( "test", "test", path=tmp_path, cache_timeout=timedelta(days=1) ) # Test without an existing artifact list(database.fetch(b"")) assert not database._disabled assert database._initialized assert database._artifact == artifact # Now we'll see if the DB also fetched correctly with an expired artifact now = datetime.now(timezone.utc) # We create an expired artifact with ga_empty_artifact(date=now - timedelta(days=2)) as ( path_with_artifact, old_artifact, ): database = GitHubArtifactDatabase( "test", "test", path=path_with_artifact, cache_timeout=timedelta(days=1) ) # Trigger initialization list(database.fetch(b"")) assert not database._disabled assert database._initialized assert database._artifact == artifact # Check that the artifact was deleted assert not old_artifact.exists() def test_ga_fallback_expired(monkeypatch): """ Tests that the fallback to an expired artifact is triggered if fetching a new one fails. This allows for (by example) offline development. """ now = datetime.now(timezone.utc) with ga_empty_artifact(date=now - timedelta(days=2)) as (path, artifact): database = GitHubArtifactDatabase( "test", "test", path=path, cache_timeout=timedelta(days=1) ) # This should trigger the fallback def fake_fetch_artifact(self) -> Path | None: return None monkeypatch.setattr( GitHubArtifactDatabase, "_fetch_artifact", fake_fetch_artifact ) # Trigger initialization with pytest.warns(HypothesisWarning): list(database.fetch(b"")) assert not database._disabled assert database._initialized assert database._artifact == artifact class GitHubArtifactMocks(RuleBasedStateMachine): """ This is a state machine that tests agreement of GitHubArtifactDatabase with DirectoryBasedExampleDatabase (as a reference implementation). """ def __init__(self): super().__init__() self.temp_directory = Path(tempfile.mkdtemp()) self.path = self.temp_directory / "github-artifacts" # This is where we will store the contents for the zip file timestamp = datetime.now(timezone.utc).isoformat().replace(":", "_") self.zip_destination = self.path / f"{timestamp}.zip" # And this is where we want to create it self.zip_content_path = self.path / timestamp self.zip_content_path.mkdir(parents=True, exist_ok=True) # We use a DirectoryBasedExampleDatabase to create the contents self.directory_db = DirectoryBasedExampleDatabase(str(self.zip_content_path)) self.zip_db = GitHubArtifactDatabase("mock", "mock", path=self.path) # Create zip file for the first time self._archive_directory_db() self.zip_db._initialize_db() def _make_zip(self, tree_path: Path, zip_path: Path): destination = zip_path.parent.absolute() / zip_path.stem make_archive( str(destination), "zip", root_dir=tree_path, ) def _archive_directory_db(self): # Delete all of the zip files in the directory for file in self.path.glob("*.zip"): file.unlink() self._make_zip(self.zip_content_path, self.zip_destination) keys = Bundle("keys") values = Bundle("values") @rule(target=keys, k=st.binary()) def k(self, k): return k @rule(target=values, v=st.binary()) def v(self, v): return v @rule(k=keys, v=values) def save(self, k, v): self.directory_db.save(k, v) self._archive_directory_db() self.zip_db = GitHubArtifactDatabase("mock", "mock", path=self.path) self.zip_db._initialize_db() @rule(k=keys) def values_agree(self, k): v1 = set(self.directory_db.fetch(k)) v2 = set(self.zip_db.fetch(k)) assert v1 == v2 def teardown(self): shutil.rmtree(self.temp_directory) TestGADReads = GitHubArtifactMocks.TestCase def test_gadb_coverage(): # Ensure that we always cover the nonempty-archive case, which can otherwise # cause rare incomplete-coverage failures. state = GitHubArtifactMocks() state.save(b"key", b"value") state.values_agree(b"key") @pytest.mark.parametrize("dirs", [[], ["subdir"]]) def test_database_directory_inaccessible(dirs, tmp_path, monkeypatch): monkeypatch.setattr( configuration, "__hypothesis_home_directory", tmp_path.joinpath(*dirs) ) try: tmp_path.chmod(0o000) with ( nullcontext() if WINDOWS else pytest.warns( HypothesisWarning, match=".*the default location is unusable" ) ): database = _db_for_path(not_set) database.save(b"fizz", b"buzz") finally: tmp_path.chmod(0o600) # So that pytest can clean up tmp_path later @skipif_emscripten def test_background_write_database(): db = BackgroundWriteDatabase(InMemoryExampleDatabase()) db.save(b"a", b"b") db.save(b"a", b"c") db.save(b"a", b"d") assert set(db.fetch(b"a")) == {b"b", b"c", b"d"} db.move(b"a", b"a2", b"b") assert set(db.fetch(b"a")) == {b"c", b"d"} assert set(db.fetch(b"a2")) == {b"b"} db.delete(b"a", b"c") assert set(db.fetch(b"a")) == {b"d"} @given(lists(nodes())) # covering examples @example(nodes_inline(True)) @example(nodes_inline(1)) @example(nodes_inline(0.0)) @example(nodes_inline(-0.0)) @example(nodes_inline("a")) @example(nodes_inline(b"a")) @example(nodes_inline(b"a" * 50)) @example(nodes_inline(b"1" * 100_000)) # really long bytes def test_nodes_roundtrips(nodes1): s1 = choices_to_bytes([n.value for n in nodes1]) assert isinstance(s1, bytes) ir2 = choices_from_bytes(s1) assert len(nodes1) == len(ir2) for n1, v2 in zip(nodes1, ir2, strict=True): assert choice_equal(n1.value, v2) s2 = choices_to_bytes(ir2) assert s1 == s2 @given(st.integers(min_value=0)) def test_uleb_128_roundtrips(n1): buffer1 = _pack_uleb128(n1) idx, n2 = _unpack_uleb128(buffer1) assert idx == len(buffer1) assert n1 == n2 def _database_conforms_to_listener_api( create_db, *, flush=None, supports_value_delete=True, parent_settings=None, ): # this function is a big mess to support a bunch of different special cases # for different databases, sorry. In return, we get one big stateful test # we can use to test the listener api for all of our databases. # # * create_db is a callable which accepts one argument (a path to a temporary # directory) and returns a database instance. # * flush is a callable which takes the instantiated db as an argument, and # is called on every step as an invariant. This lets the database do things # like, time.sleep to give time for events to fire. # * suports_value_delete is True if the db supports passing # the exact value of a deleted key in "delete" events. The directory database # notably does not support this, and passes None instead. @settings(parent_settings, suppress_health_check=[HealthCheck.too_slow]) class TestDatabaseListener(RuleBasedStateMachine): # this tests that if we call .delete, .save, or .move in a database, and # that operation changes the state of the database, any registered listeners # get called a corresponding number of times. keys = Bundle("keys") values = Bundle("values") def __init__(self): super().__init__() self.temp_dir = Path(tempfile.mkdtemp()) self.db = create_db(self.temp_dir) self.expected_events = [] self.actual_events = [] def listener(event): self.actual_events.append(event) self.listener = listener self.active_listeners = [] self.add_listener() def _expect_event(self, event_type, args): for _ in range(len(self.active_listeners)): self.expected_events.append((event_type, args)) def _expect_delete(self, k, v): if not supports_value_delete: v = None self._expect_event("delete", (k, v)) def _expect_save(self, k, v): self._expect_event("save", (k, v)) @rule(target=keys, k=st.binary()) def k(self, k): return k @rule(target=values, v=st.binary()) def v(self, v): return v @precondition(lambda self: not self.active_listeners) @rule() def add_listener(self): self.db.add_listener(self.listener) # wait for watchdog to initialize the listener asynchronously time_sleep(0.1) self.active_listeners.append(self.listener) @precondition(lambda self: self.listener in self.active_listeners) @rule() def remove_listener(self): self.db.remove_listener(self.listener) self.active_listeners.remove(self.listener) @rule() def clear_listeners(self): self.db.clear_listeners() self.active_listeners.clear() @rule(k=keys) def fetch(self, k): # we don't expect this to do anything, but that's the point. if this # fires a listener call then that's bad and will fail. self.db.fetch(k) @rule(k=keys, v=values) def save(self, k, v): changed = v not in set(self.db.fetch(k)) self.db.save(k, v) if changed: self._expect_save(k, v) @rule(k=keys, v=values) def delete(self, k, v): changed = v in set(self.db.fetch(k)) self.db.delete(k, v) if changed: self._expect_delete(k, v) @rule(k1=keys, k2=keys, v=values) def move(self, k1, k2, v): in_k1 = v in set(self.db.fetch(k1)) save_changed = v not in set(self.db.fetch(k2)) delete_changed = k1 != k2 and in_k1 self.db.move(k1, k2, v) # A move gets emitted as a delete followed by a save. The # delete may be omitted if k1==k2, and the save if v in db.fetch(k2). if delete_changed: self._expect_delete(k1, v) if save_changed: self._expect_save(k2, v) # it would be nice if this was an @rule, but that runs into race condition # failures where an event listener is removed immediately after a # save/delete/move operation, before the listener can fire. This is only # relevant for DirectoryBasedExampleDatabase. @invariant() def events_agree(self): if flush is not None: flush(self.db) wait_for( lambda: len(self.expected_events) == len(self.actual_events), timeout=60 ) # events *generally* don't arrive out of order, but we've had # flakes reported here, especially on weirder / older machines. # see https://github.com/HypothesisWorks/hypothesis/issues/4274 assert Counter(self.expected_events) == Counter(self.actual_events) def teardown(self): shutil.rmtree(self.temp_dir) run_state_machine_as_test(TestDatabaseListener) def test_database_listener_memory(): _database_conforms_to_listener_api( lambda path: InMemoryExampleDatabase(), parent_settings=settings(max_examples=5, stateful_step_count=10), ) @skipif_emscripten @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ages" ) def test_database_listener_background_write(): _database_conforms_to_listener_api( lambda path: BackgroundWriteDatabase(InMemoryExampleDatabase()), flush=lambda db: db._join(), parent_settings=settings(max_examples=5, stateful_step_count=10), ) def test_can_remove_nonexistent_listener(): db = InMemoryExampleDatabase() db.remove_listener(lambda event: event) class DoesNotSupportListening(ExampleDatabase): def save(self, key: bytes, value: bytes) -> None: ... def fetch(self, key: bytes) -> Iterable[bytes]: ... def delete(self, key: bytes, value: bytes) -> None: ... def test_warns_when_listening_not_supported(): db = DoesNotSupportListening() listener = lambda event: event with pytest.warns( HypothesisWarning, match="does not support listening for changes" ): db.add_listener(listener) with pytest.warns( HypothesisWarning, match="does not support stopping listening for changes" ): db.remove_listener(listener) def test_readonly_listener(): db = ReadOnlyDatabase(InMemoryExampleDatabase()) def listener(event): raise AssertionError("ReadOnlyDatabase never fires change events") db.add_listener(listener) db.save(b"a", b"a") db.remove_listener(listener) db.save(b"b", b"b") @skipif_threading def test_metakeys_move_into_existing_key(tmp_path): db = DirectoryBasedExampleDatabase(tmp_path) db.save(b"k1", b"v1") db.save(b"k1", b"v2") db.save(b"k2", b"v3") assert set(db.fetch(db._metakeys_name)) == {b"k1", b"k2"} db.move(b"k1", b"k2", b"v2") assert set(db.fetch(db._metakeys_name)) == {b"k1", b"k2"} @skipif_threading def test_metakeys_move_into_nonexistent_key(tmp_path): db = DirectoryBasedExampleDatabase(tmp_path) db.save(b"k1", b"v1") assert set(db.fetch(db._metakeys_name)) == {b"k1"} db.move(b"k1", b"k2", b"v1") assert set(db.fetch(db._metakeys_name)) == {b"k1", b"k2"} @skipif_threading def test_metakeys(tmp_path): db = DirectoryBasedExampleDatabase(tmp_path) db.save(b"k1", b"v1") assert set(db.fetch(db._metakeys_name)) == {b"k1"} db.save(b"k1", b"v2") assert set(db.fetch(db._metakeys_name)) == {b"k1"} # deleting all the values from a key removes that metakey db.delete(b"k1", b"v1") db.delete(b"k1", b"v2") assert set(db.fetch(db._metakeys_name)) == set() db.save(b"k2", b"v1") assert set(db.fetch(db._metakeys_name)) == {b"k2"} class TracksListens(ExampleDatabase): def __init__(self): super().__init__() self.starts = 0 self.ends = 0 def save(self, key: bytes, value: bytes) -> None: ... def fetch(self, key: bytes) -> Iterable[bytes]: ... def delete(self, key: bytes, value: bytes) -> None: ... def _start_listening(self): self.starts += 1 def _stop_listening(self): self.ends += 1 def test_start_end_listening(): db = TracksListens() def listener1(event): pass def listener2(event): pass assert db.starts == 0 db.add_listener(listener1) assert db.starts == 1 db.add_listener(listener2) assert db.starts == 1 assert db.ends == 0 db.remove_listener(listener2) assert db.ends == 0 db.remove_listener(listener1) assert db.ends == 1 db.clear_listeners() assert db.ends == 1 @checks_deprecated_behaviour def test_deprecated_example_database_path(tmp_path): ExampleDatabase(tmp_path) @checks_deprecated_behaviour def test_deprecated_example_database_memory(): ExampleDatabase(":memory:") @checks_deprecated_behaviour def test_deprecated_example_database_no_args(): ExampleDatabase() @pytest.mark.parametrize( "db1, db2", [ (DirectoryBasedExampleDatabase("a"), DirectoryBasedExampleDatabase("a")), ( MultiplexedDatabase( DirectoryBasedExampleDatabase("a"), DirectoryBasedExampleDatabase("b") ), MultiplexedDatabase( DirectoryBasedExampleDatabase("a"), DirectoryBasedExampleDatabase("b") ), ), ( ReadOnlyDatabase(DirectoryBasedExampleDatabase("a")), ReadOnlyDatabase(DirectoryBasedExampleDatabase("a")), ), ( GitHubArtifactDatabase("owner1", "repo1"), GitHubArtifactDatabase("owner1", "repo1"), ), ], ) def test_database_equal(db1, db2): assert db1 == db2 @pytest.mark.parametrize( "db1, db2", [ (InMemoryExampleDatabase(), InMemoryExampleDatabase()), (InMemoryExampleDatabase(), DirectoryBasedExampleDatabase("a")), (BackgroundWriteDatabase(InMemoryExampleDatabase()), InMemoryExampleDatabase()), (DirectoryBasedExampleDatabase("a"), DirectoryBasedExampleDatabase("b")), ( ReadOnlyDatabase(DirectoryBasedExampleDatabase("a")), ReadOnlyDatabase(DirectoryBasedExampleDatabase("b")), ), ( GitHubArtifactDatabase("owner1", "repo1"), GitHubArtifactDatabase("owner2", "repo2"), ), ], ) def test_database_not_equal(db1, db2): assert db1 != db2 @skipif_threading # race in tmp_path def test_directory_db_removes_empty_dirs(tmp_path): db = DirectoryBasedExampleDatabase(tmp_path) db.save(b"k1", b"v1") db.save(b"k1", b"v2") assert db._key_path(b"k1").exists() assert set(db.fetch(db._metakeys_name)) == {b"k1"} db.delete(b"k1", b"v1") assert db._key_path(b"k1").exists() assert set(db.fetch(db._metakeys_name)) == {b"k1"} db.delete(b"k1", b"v2") assert not db._key_path(b"k1").exists() assert set(db.fetch(db._metakeys_name)) == set() ================================================ FILE: hypothesis-python/tests/cover/test_datetimes.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime as dt import pytest from hypothesis import HealthCheck, given, settings from hypothesis.strategies import dates, datetimes, timedeltas, times from tests.common.debug import assert_simple_property, find_any, minimal def test_can_find_positive_delta(): assert minimal(timedeltas(), lambda x: x.days > 0) == dt.timedelta(1) def test_can_find_negative_delta(): assert minimal( timedeltas(max_value=dt.timedelta(10**6)), lambda x: x.days < 0 ) == dt.timedelta(-1) def test_can_find_on_the_second(): find_any(timedeltas(), lambda x: x.seconds == 0) def test_can_find_off_the_second(): find_any(timedeltas(), lambda x: x.seconds != 0) def test_simplifies_towards_zero_delta(): d = minimal(timedeltas()) assert d.days == d.seconds == d.microseconds == 0 def test_min_value_is_respected(): assert minimal(timedeltas(min_value=dt.timedelta(days=10))).days == 10 def test_max_value_is_respected(): assert minimal(timedeltas(max_value=dt.timedelta(days=-10))).days == -10 @settings(suppress_health_check=list(HealthCheck)) @given(timedeltas()) def test_single_timedelta(val): assert_simple_property(timedeltas(val, val), lambda v: v is val) def test_simplifies_towards_millenium(): d = minimal(datetimes()) assert d.year == 2000 assert d.month == d.day == 1 assert d.hour == d.minute == d.second == d.microsecond == 0 @given(datetimes()) def test_default_datetimes_are_naive(dt): assert dt.tzinfo is None def test_bordering_on_a_leap_year(): x = minimal( datetimes( dt.datetime.min.replace(year=2003), dt.datetime.max.replace(year=2005) ), lambda x: x.month == 2 and x.day == 29, settings=settings(max_examples=2500), ) assert x.year == 2004 def test_can_find_after_the_year_2000(): assert minimal(dates(), lambda x: x.year > 2000).year == 2001 def test_can_find_before_the_year_2000(): assert minimal(dates(), lambda x: x.year < 2000).year == 1999 @pytest.mark.parametrize("month", range(1, 13)) def test_can_find_each_month(month): find_any(dates(), lambda x: x.month == month, settings(max_examples=10**6)) def test_min_year_is_respected(): assert minimal(dates(min_value=dt.date.min.replace(2003))).year == 2003 def test_max_year_is_respected(): assert minimal(dates(max_value=dt.date.min.replace(1998))).year == 1998 @given(dates()) def test_single_date(val): assert find_any(dates(val, val)) is val def test_can_find_midnight(): find_any(times(), lambda x: x.hour == x.minute == x.second == 0) def test_can_find_non_midnight(): assert minimal(times(), lambda x: x.hour != 0).hour == 1 def test_can_find_on_the_minute(): find_any(times(), lambda x: x.second == 0) def test_can_find_off_the_minute(): find_any(times(), lambda x: x.second != 0) def test_simplifies_towards_midnight(): d = minimal(times()) assert d.hour == d.minute == d.second == d.microsecond == 0 def test_can_generate_naive_time(): find_any(times(), lambda d: not d.tzinfo) @given(times()) def test_naive_times_are_naive(dt): assert dt.tzinfo is None def test_can_generate_datetime_with_fold_1(): find_any(datetimes(), lambda d: d.fold) def test_can_generate_time_with_fold_1(): find_any(times(), lambda d: d.fold) @given(datetimes(allow_imaginary=False)) def test_allow_imaginary_is_not_an_error_for_naive_datetimes(d): pass ================================================ FILE: hypothesis-python/tests/cover/test_deadline.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import gc import re import time import pytest from hypothesis import given, settings, strategies as st from hypothesis.errors import DeadlineExceeded, FlakyFailure, InvalidArgument from tests.common.utils import assert_falsifying_output, fails_with pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason="takes a long time because we don't monkeypatch time", ) def test_raises_deadline_on_slow_test(): @settings(deadline=500) @given(st.integers()) def slow(i): time.sleep(1) with pytest.raises(DeadlineExceeded): slow() @pytest.mark.skipif( settings().deadline is None, reason="not expected to fail if deadline is disabled", ) @fails_with(DeadlineExceeded) @given(st.integers()) def test_slow_tests_are_errors_by_default(i): time.sleep(1) def test_non_numeric_deadline_is_an_error(): with pytest.raises(InvalidArgument): settings(deadline="3 seconds") @given(st.integers()) @settings(deadline=None) def test_slow_with_none_deadline(i): time.sleep(1) def test_raises_flaky_if_a_test_becomes_fast_on_rerun(): once = True @settings(deadline=500, backend="hypothesis") @given(st.integers()) def test_flaky_slow(i): nonlocal once if once: once = False time.sleep(1) with pytest.raises(FlakyFailure): test_flaky_slow() def test_deadlines_participate_in_shrinking(): @settings(deadline=500, max_examples=1000, database=None) @given(st.integers(min_value=0)) def slow_if_large(i): if i >= 1000: time.sleep(1) assert_falsifying_output( slow_if_large, expected_exception=DeadlineExceeded, i=1000, ) def test_keeps_you_well_above_the_deadline(): seen = set() failed_once = False @settings(deadline=100, backend="hypothesis") @given(st.integers(0, 2000)) def slow(i): nonlocal failed_once # Make sure our initial failure isn't something that immediately goes flaky. if not failed_once: if i * 0.9 <= 100: return else: failed_once = True t = i / 1000 if i in seen: time.sleep(0.9 * t) else: seen.add(i) time.sleep(t) with pytest.raises(DeadlineExceeded): slow() def test_gives_a_deadline_specific_flaky_error_message(): once = True @settings(deadline=100, backend="hypothesis") @given(st.integers()) def slow_once(i): nonlocal once if once: once = False time.sleep(0.2) with pytest.raises(FlakyFailure) as err: slow_once() assert "Unreliable test timing" in "\n".join(err.value.__notes__) # this used to be "took 2", but we saw that flake (on pypy, though unsure if # that means anything) with "took 199.59ms". It's possible our gc accounting # is incorrect, or we could just be running into rare non-guarantees of # time.sleep. assert re.search(r"took \d", "\n".join(err.value.__notes__)) @pytest.mark.parametrize("slow_strategy", [False, True]) @pytest.mark.parametrize("slow_test", [False, True]) def test_should_only_fail_a_deadline_if_the_test_is_slow(slow_strategy, slow_test): s = st.integers() if slow_strategy: s = s.map(lambda x: time.sleep(0.08)) @settings(deadline=50) @given(st.data()) def test(data): data.draw(s) if slow_test: time.sleep(0.1) if slow_test: with pytest.raises(DeadlineExceeded): test() else: test() @pytest.mark.skipif(not hasattr(gc, "callbacks"), reason="CPython specific gc delay") def test_should_not_fail_deadline_due_to_gc(): @settings(max_examples=1, deadline=50) @given(st.integers()) def test(i): before = time.perf_counter() gc.collect() assert time.perf_counter() - before >= 0.1 # verify that we're slow def delay(phase, _info): if phase == "start": time.sleep(0.1) try: gc.callbacks.append(delay) test() finally: gc.callbacks.remove(delay) ================================================ FILE: hypothesis-python/tests/cover/test_debug_information.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import pytest from hypothesis import Verbosity, given, settings, strategies as st from hypothesis.database import InMemoryExampleDatabase from tests.common.utils import capture_out def test_reports_passes(): @given(st.integers()) @settings( verbosity=Verbosity.debug, max_examples=1000, database=InMemoryExampleDatabase() ) def test(i): assert i < 10 with capture_out() as out, pytest.raises(AssertionError): test() value = out.getvalue() assert "minimize_individual_choices" in value assert "calls" in value assert "shrinks" in value shrinks_info = re.compile(r"call(s?) of which ([0-9]+) shrank") for l in value.splitlines(): m = shrinks_info.search(l) if m is not None and int(m.group(2)) != 0: break else: pytest.xfail(reason="Sometimes the first failure is 10, and cannot shrink.") ================================================ FILE: hypothesis-python/tests/cover/test_deferred_strategies.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument from tests.common.debug import assert_no_examples, check_can_generate_examples, minimal def test_binary_tree(): tree = st.deferred(lambda: st.integers() | st.tuples(tree, tree)) assert minimal(tree) == 0 assert minimal(tree, lambda x: isinstance(x, tuple)) == (0, 0) def test_mutual_recursion(): t = st.deferred(lambda: a | b) a = st.deferred(lambda: st.none() | st.tuples(st.just("a"), b)) b = st.deferred(lambda: st.none() | st.tuples(st.just("b"), a)) for c in ("a", "b"): assert minimal(t, lambda x: x is not None and x[0] == c) == (c, None) # noqa def test_errors_on_non_function_define(): x = st.deferred(1) with pytest.raises(InvalidArgument): check_can_generate_examples(x) def test_errors_if_define_does_not_return_search_strategy(): x = st.deferred(lambda: 1) with pytest.raises(InvalidArgument): check_can_generate_examples(x) def test_errors_on_definition_as_self(): x = st.deferred(lambda: x) with pytest.raises(InvalidArgument): check_can_generate_examples(x) def test_branches_pass_through_deferred(): x = st.one_of(st.booleans(), st.integers()) y = st.deferred(lambda: x) assert x.branches == y.branches def test_can_draw_one_of_self(): x = st.deferred(lambda: st.one_of(st.booleans(), x)) assert minimal(x) is False assert len(x.branches) == 1 def test_hidden_self_references_just_result_in_no_example(): bad = st.deferred(lambda: st.none().flatmap(lambda _: bad)) assert_no_examples(bad) def test_self_recursive_flatmap(): bad = st.deferred(lambda: bad.flatmap(lambda x: st.none())) assert_no_examples(bad) def test_self_reference_through_one_of_can_detect_emptiness(): bad = st.deferred(lambda: st.one_of(bad, bad)) assert bad.is_empty def test_self_tuple_draws_nothing(): x = st.deferred(lambda: st.tuples(x)) assert_no_examples(x) def test_mutually_recursive_tuples_draw_nothing(): x = st.deferred(lambda: st.tuples(y)) y = st.tuples(x) assert_no_examples(x) assert_no_examples(y) def test_literals_strategy_is_valid(): literals = st.deferred( lambda: st.one_of( st.booleans(), st.tuples(literals, literals), literals.map(lambda x: [x]) ) ) @given(literals) def test(e): pass test() assert not literals.has_reusable_values def test_impossible_self_recursion(): x = st.deferred(lambda: st.tuples(st.none(), x)) assert x.is_empty assert x.has_reusable_values def test_very_deep_deferral(): # This test is designed so that the recursive properties take a very long # time to converge: Although we can rapidly determine them for the original # value, each round in the fixed point calculation only manages to update # a single value in the related strategies, so it takes 100 rounds to # update everything. Most importantly this triggers our infinite loop # detection heuristic and we start tracking duplicates, but we shouldn't # see any because this loop isn't infinite, just long. def strat(i): if i == 0: return st.deferred(lambda: st.one_of([*strategies, st.none()])) else: return st.deferred(lambda: st.tuples(strategies[(i + 1) % len(strategies)])) strategies = list(map(strat, range(100))) assert strategies[0].has_reusable_values assert not strategies[0].is_empty def test_recursion_in_middle(): # This test is significant because the integers().map(abs) is not checked # in the initial pass - when we recurse into x initially we decide that # x is empty, so the tuple is empty, and don't need to check the third # argument. Then when we do the more refined test we've discovered that x # is non-empty, so we need to check the non-emptiness of the last component # to determine the non-emptiness of the tuples. x = st.deferred(lambda: st.tuples(st.none(), x, st.integers().map(abs)) | st.none()) assert not x.is_empty ================================================ FILE: hypothesis-python/tests/cover/test_detection.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, is_hypothesis_test from hypothesis.stateful import RuleBasedStateMachine, rule from hypothesis.strategies import integers def test_functions_default_to_not_tests(): def foo(): pass assert not is_hypothesis_test(foo) def test_methods_default_to_not_tests(): class Foo: def foo(self): pass assert not is_hypothesis_test(Foo().foo) def test_detection_of_functions(): @given(integers()) def test(i): pass assert is_hypothesis_test(test) def test_detection_of_methods(): class Foo: @given(integers()) def test(self, i): pass assert is_hypothesis_test(Foo().test) def test_detection_of_stateful_tests(): class Stuff(RuleBasedStateMachine): @rule(x=integers()) def a_rule(self, x): pass assert is_hypothesis_test(Stuff.TestCase().runTest) ================================================ FILE: hypothesis-python/tests/cover/test_direct_strategies.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import collections import decimal import enum import fractions import math import warnings from datetime import date, datetime, time, timedelta from ipaddress import IPv4Network, IPv6Network import pytest from hypothesis import Phase, given, settings, strategies as st from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.vendor.pretty import pretty from tests.common.debug import check_can_generate_examples, minimal from tests.common.utils import Why, xfail_on_crosshair # Use `pretty` instead of `repr` for building test names, so that set and dict # parameters print consistently across multiple worker processes with different # PYTHONHASHSEED values. def fn_test(*fnkwargs): fnkwargs = list(fnkwargs) return pytest.mark.parametrize( ("fn", "args"), fnkwargs, ids=[ "{}({})".format(fn.__name__, ", ".join(map(pretty, args))) for fn, args in fnkwargs ], ) def fn_ktest(*fnkwargs): fnkwargs = list(fnkwargs) return pytest.mark.parametrize( ("fn", "kwargs"), fnkwargs, ids=[f"{fn.__name__}(**{pretty(kwargs)})" for fn, kwargs in fnkwargs], ) @fn_ktest( (st.integers, {"min_value": math.nan}), (st.integers, {"min_value": 2, "max_value": 1}), (st.integers, {"min_value": math.nan}), (st.integers, {"max_value": math.nan}), (st.integers, {"min_value": decimal.Decimal("1.5")}), (st.integers, {"max_value": decimal.Decimal("1.5")}), (st.integers, {"min_value": -1.5, "max_value": -0.5}), (st.integers, {"min_value": 0.1, "max_value": 0.2}), (st.dates, {"min_value": "fish"}), (st.dates, {"max_value": "fish"}), (st.dates, {"min_value": date(2017, 8, 22), "max_value": date(2017, 8, 21)}), (st.datetimes, {"min_value": "fish"}), (st.datetimes, {"max_value": "fish"}), (st.datetimes, {"allow_imaginary": 0}), ( st.datetimes, {"min_value": datetime(2017, 8, 22), "max_value": datetime(2017, 8, 21)}, ), (st.decimals, {"min_value": math.nan}), (st.decimals, {"max_value": math.nan}), (st.decimals, {"min_value": 2, "max_value": 1}), (st.decimals, {"max_value": "-snan"}), (st.decimals, {"max_value": complex(1, 2)}), (st.decimals, {"places": -1}), (st.decimals, {"places": 0.5}), (st.decimals, {"max_value": 0.0, "min_value": 1.0}), (st.decimals, {"min_value": 1.0, "max_value": 0.0}), (st.decimals, {"min_value": 0.0, "max_value": 1.0, "allow_infinity": True}), (st.decimals, {"min_value": "inf"}), (st.decimals, {"max_value": "-inf"}), (st.decimals, {"min_value": "-inf", "allow_infinity": False}), (st.decimals, {"max_value": "inf", "allow_infinity": False}), (st.decimals, {"min_value": complex(1, 2)}), (st.decimals, {"min_value": "0.1", "max_value": "0.9", "places": 0}), (st.decimals, {"min_value": fractions.Fraction(1, 3)}), (st.decimals, {"max_value": fractions.Fraction(2, 3), "places": 1}), ( st.dictionaries, {"keys": st.booleans(), "values": st.booleans(), "min_size": 10, "max_size": 1}, ), (st.floats, {"min_value": math.nan}), (st.floats, {"max_value": math.nan}), (st.floats, {"min_value": complex(1, 2)}), (st.floats, {"max_value": complex(1, 2)}), (st.floats, {"exclude_min": None}), (st.floats, {"exclude_max": None}), (st.floats, {"exclude_min": True}), # because min_value=None (st.floats, {"exclude_max": True}), # because max_value=None (st.floats, {"min_value": 1.8, "width": 32}), (st.floats, {"max_value": 1.8, "width": 32}), (st.fractions, {"min_value": 2, "max_value": 1}), (st.fractions, {"min_value": math.nan}), (st.fractions, {"max_value": math.nan}), (st.fractions, {"max_denominator": 0}), (st.fractions, {"max_denominator": 1.5}), (st.fractions, {"min_value": complex(1, 2)}), (st.fractions, {"min_value": "1/3", "max_value": "1/2", "max_denominator": 2}), (st.fractions, {"min_value": "0", "max_value": "1/3", "max_denominator": 2}), (st.fractions, {"min_value": "1/3", "max_value": "1/3", "max_denominator": 2}), (st.lists, {"elements": st.integers(), "min_size": 10, "max_size": 9}), (st.lists, {"elements": st.integers(), "min_size": -10, "max_size": -9}), (st.lists, {"elements": st.integers(), "max_size": -9}), (st.lists, {"elements": st.integers(), "min_size": -10}), (st.lists, {"elements": st.integers(), "min_size": math.nan}), (st.lists, {"elements": st.nothing(), "max_size": 1}), (st.lists, {"elements": "hi"}), (st.lists, {"elements": st.integers(), "unique_by": 1}), (st.lists, {"elements": st.integers(), "unique_by": ()}), (st.lists, {"elements": st.integers(), "unique_by": (1,)}), (st.lists, {"elements": st.sampled_from([0, 1]), "min_size": 3, "unique": True}), (st.lists, {"elements": st.none(), "min_size": 100_000}), (st.lists, {"elements": st.none(), "min_size": 100_000, "unique": True}), ( st.lists, {"elements": st.sampled_from([1, 2]), "min_size": 100_000, "unique": True}, ), (st.text, {"min_size": 10, "max_size": 9}), (st.text, {"alphabet": [1]}), (st.text, {"alphabet": ["abc"]}), (st.text, {"alphabet": st.just("abc")}), (st.text, {"alphabet": st.sampled_from(["abc", "def"])}), (st.text, {"alphabet": st.just(123)}), (st.text, {"alphabet": st.sampled_from([123, 456])}), (st.text, {"alphabet": st.builds(lambda: "abc")}), (st.text, {"alphabet": st.builds(lambda: 123)}), (st.text, {"alphabet": "abc", "min_size": 100_000}), (st.from_regex, {"regex": 123}), (st.from_regex, {"regex": b"abc", "alphabet": "abc"}), (st.from_regex, {"regex": b"abc", "alphabet": b"def"}), (st.from_regex, {"regex": "abc", "alphabet": "def"}), (st.from_regex, {"regex": "aa|bb", "alphabet": "c"}), (st.from_regex, {"regex": "[abc]", "alphabet": "def"}), (st.from_regex, {"regex": "[ab]x[de]", "alphabet": "abcdef"}), (st.from_regex, {"regex": "...", "alphabet": st.builds(lambda: "a")}), (st.from_regex, {"regex": "abc", "alphabet": st.sampled_from("def")}), (st.from_regex, {"regex": "abc", "alphabet": st.characters(min_codepoint=128)}), (st.from_regex, {"regex": "abc", "alphabet": 123}), (st.binary, {"min_size": 10, "max_size": 9}), (st.floats, {"min_value": math.nan}), (st.floats, {"min_value": "0"}), (st.floats, {"max_value": "0"}), (st.floats, {"min_value": 0.0, "max_value": -0.0}), (st.floats, {"min_value": 0.0, "max_value": 1.0, "allow_infinity": True}), (st.floats, {"max_value": 0.0, "min_value": 1.0}), (st.floats, {"min_value": 0.0, "allow_nan": True}), (st.floats, {"max_value": 0.0, "allow_nan": True}), (st.floats, {"min_value": 0.0, "max_value": 1.0, "allow_infinity": True}), (st.floats, {"min_value": math.inf, "allow_infinity": False}), (st.floats, {"max_value": -math.inf, "allow_infinity": False}), (st.complex_numbers, {"min_magnitude": None}), (st.complex_numbers, {"min_magnitude": math.nan}), (st.complex_numbers, {"max_magnitude": math.nan}), (st.complex_numbers, {"max_magnitude": complex(1, 2)}), (st.complex_numbers, {"min_magnitude": -1}), (st.complex_numbers, {"max_magnitude": -1}), (st.complex_numbers, {"min_magnitude": 3, "max_magnitude": 2}), (st.complex_numbers, {"max_magnitude": 2, "allow_infinity": True}), (st.complex_numbers, {"max_magnitude": 2, "allow_nan": True}), (st.complex_numbers, {"width": None}), # Conceivable mistake when misunderstanding width for individual component widths: (st.complex_numbers, {"width": 16}), # Unsupported as of now: (st.complex_numbers, {"width": 196}), (st.complex_numbers, {"width": 256}), (st.fixed_dictionaries, {"mapping": "fish"}), (st.fixed_dictionaries, {"mapping": {1: "fish"}}), (st.fixed_dictionaries, {"mapping": {}, "optional": "fish"}), (st.fixed_dictionaries, {"mapping": {}, "optional": {1: "fish"}}), (st.fixed_dictionaries, {"mapping": {}, "optional": collections.OrderedDict()}), (st.fixed_dictionaries, {"mapping": {1: st.none()}, "optional": {1: st.none()}}), (st.dictionaries, {"keys": st.integers(), "values": 1}), (st.dictionaries, {"keys": 1, "values": st.integers()}), (st.text, {"alphabet": "", "min_size": 1}), (st.timedeltas, {"min_value": "fish"}), (st.timedeltas, {"max_value": "fish"}), ( st.timedeltas, {"min_value": timedelta(hours=1), "max_value": timedelta(minutes=1)}, ), (st.times, {"min_value": "fish"}), (st.times, {"max_value": "fish"}), (st.times, {"min_value": time(2, 0), "max_value": time(1, 0)}), (st.uuids, {"version": 6}), (st.characters, {"min_codepoint": -1}), (st.characters, {"min_codepoint": "1"}), (st.characters, {"max_codepoint": -1}), (st.characters, {"max_codepoint": "1"}), (st.characters, {"categories": []}), (st.characters, {"categories": ["Nd"], "exclude_categories": ["Nd"]}), (st.characters, {"whitelist_categories": ["Nd"], "blacklist_categories": ["Nd"]}), (st.characters, {"include_characters": "a", "blacklist_characters": "b"}), (st.characters, {"codec": 100}), (st.characters, {"codec": "this is not a valid codec name"}), (st.characters, {"codec": "ascii", "include_characters": "é"}), (st.characters, {"codec": "utf-8", "categories": "Cs"}), (st.slices, {"size": None}), (st.slices, {"size": "chips"}), (st.slices, {"size": -1}), (st.slices, {"size": 2.3}), (st.sampled_from, {"elements": ()}), (st.ip_addresses, {"v": "4"}), (st.ip_addresses, {"v": 4.0}), (st.ip_addresses, {"v": 5}), (st.ip_addresses, {"v": 4, "network": "::/64"}), (st.ip_addresses, {"v": 6, "network": "127.0.0.0/8"}), (st.ip_addresses, {"network": b"127.0.0.0/8"}), # only unicode strings are valid (st.ip_addresses, {"network": b"::/64"}), (st.randoms, {"use_true_random": "False"}), (st.randoms, {"note_method_calls": "True"}), ) def test_validates_keyword_arguments(fn, kwargs): with pytest.raises(InvalidArgument): check_can_generate_examples(fn(**kwargs)) @fn_ktest( (st.integers, {"min_value": 0}), (st.integers, {"min_value": 11}), (st.integers, {"min_value": 11, "max_value": 100}), (st.integers, {"max_value": 0}), (st.integers, {"min_value": -2, "max_value": -1}), (st.decimals, {"min_value": 1.0, "max_value": 1.5}), (st.decimals, {"min_value": "1.0", "max_value": "1.5"}), (st.decimals, {"min_value": decimal.Decimal("1.5")}), (st.decimals, {"max_value": 1.0, "min_value": -1.0, "allow_infinity": False}), (st.decimals, {"min_value": 1.0, "allow_nan": False}), (st.decimals, {"max_value": 1.0, "allow_nan": False}), (st.decimals, {"max_value": 1.0, "min_value": -1.0, "allow_nan": False}), (st.decimals, {"min_value": "-inf"}), (st.decimals, {"max_value": "inf"}), (st.decimals, {"min_value": fractions.Fraction(3, 20)}), (st.decimals, {"max_value": fractions.Fraction(1, 8), "places": 10}), (st.fractions, {"min_value": -1, "max_value": 1, "max_denominator": 1000}), (st.fractions, {"min_value": 1, "max_value": 1}), (st.fractions, {"min_value": 1, "max_value": 1, "max_denominator": 2}), (st.fractions, {"min_value": 1.0}), (st.fractions, {"min_value": decimal.Decimal("1.0")}), (st.fractions, {"min_value": fractions.Fraction(1, 2)}), (st.fractions, {"min_value": "1/2", "max_denominator": 2}), (st.fractions, {"max_value": "1/2", "max_denominator": 3}), (st.lists, {"elements": st.nothing(), "max_size": 0}), (st.lists, {"elements": st.integers()}), (st.lists, {"elements": st.integers(), "max_size": 5}), (st.lists, {"elements": st.booleans(), "min_size": 5}), (st.lists, {"elements": st.booleans(), "min_size": 5, "max_size": 10}), (st.sets, {"min_size": 10, "max_size": 10, "elements": st.integers()}), (st.booleans, {}), (st.just, {"value": "hi"}), (st.integers, {"min_value": 12, "max_value": 12}), (st.floats, {}), (st.floats, {"min_value": 1.0}), (st.floats, {"max_value": 1.0}), (st.floats, {"min_value": math.inf}), (st.floats, {"max_value": -math.inf}), (st.floats, {"max_value": 1.0, "min_value": -1.0}), (st.floats, {"max_value": 1.0, "min_value": -1.0, "allow_infinity": False}), (st.floats, {"min_value": 1.0, "allow_nan": False}), (st.floats, {"max_value": 1.0, "allow_nan": False}), (st.floats, {"max_value": 1.0, "min_value": -1.0, "allow_nan": False}), (st.complex_numbers, {}), (st.complex_numbers, {"min_magnitude": 3, "max_magnitude": 3}), (st.complex_numbers, {"max_magnitude": 0}), (st.complex_numbers, {"allow_nan": True}), (st.complex_numbers, {"allow_nan": True, "allow_infinity": True}), (st.complex_numbers, {"allow_nan": True, "allow_infinity": False}), (st.complex_numbers, {"allow_nan": False}), (st.complex_numbers, {"allow_nan": False, "allow_infinity": True}), (st.complex_numbers, {"allow_nan": False, "allow_infinity": False}), (st.complex_numbers, {"max_magnitude": math.inf, "allow_infinity": True}), (st.complex_numbers, {"width": 32}), (st.complex_numbers, {"width": 64}), (st.complex_numbers, {"width": 128}), (st.sampled_from, {"elements": [1]}), (st.sampled_from, {"elements": [1, 2, 3]}), (st.fixed_dictionaries, {"mapping": {1: st.integers()}}), (st.dictionaries, {"keys": st.booleans(), "values": st.integers()}), (st.text, {"alphabet": "abc"}), (st.text, {"alphabet": set("abc")}), (st.text, {"alphabet": ""}), (st.text, {"alphabet": st.just("a")}), (st.text, {"alphabet": st.sampled_from("abc")}), (st.text, {"alphabet": st.builds(lambda: "a")}), (st.characters, {"codec": "ascii"}), (st.characters, {"codec": "latin1"}), (st.characters, {"categories": ["N"]}), (st.characters, {"exclude_categories": []}), (st.characters, {"whitelist_characters": "a", "codec": "ascii"}), (st.characters, {"blacklist_characters": "a"}), (st.characters, {"whitelist_categories": ["Nd"]}), (st.characters, {"blacklist_categories": ["Nd"]}), (st.from_regex, {"regex": "abc", "alphabet": "abc"}), (st.from_regex, {"regex": "abc", "alphabet": "abcdef"}), (st.from_regex, {"regex": "[abc]", "alphabet": "abcdef"}), (st.from_regex, {"regex": "[a-f]", "alphabet": "abef"}), (st.from_regex, {"regex": "[a-d]", "alphabet": "def"}), (st.from_regex, {"regex": "[f-z]", "alphabet": "def"}), (st.from_regex, {"regex": "abc", "alphabet": st.sampled_from("abc")}), (st.from_regex, {"regex": "abc", "alphabet": st.characters(codec="ascii")}), (st.ip_addresses, {}), (st.ip_addresses, {"v": 4}), (st.ip_addresses, {"v": 6}), (st.ip_addresses, {"network": "127.0.0.0/8"}), (st.ip_addresses, {"network": "::/64"}), (st.ip_addresses, {"v": 4, "network": "127.0.0.0/8"}), (st.ip_addresses, {"v": 6, "network": "::/64"}), (st.ip_addresses, {"network": IPv4Network("127.0.0.0/8")}), (st.ip_addresses, {"network": IPv6Network("::/64")}), (st.ip_addresses, {"v": 4, "network": IPv4Network("127.0.0.0/8")}), (st.ip_addresses, {"v": 6, "network": IPv6Network("::/64")}), ) def test_produces_valid_examples_from_keyword(fn, kwargs): check_can_generate_examples(fn(**kwargs)) @fn_test( (st.one_of, (1,)), (st.one_of, (1, st.integers())), (st.tuples, (1,)), ) def test_validates_args(fn, args): with pytest.raises(InvalidArgument): check_can_generate_examples(fn(*args)) @fn_test( (st.one_of, (st.booleans(), st.tuples(st.booleans()))), (st.one_of, (st.booleans(),)), (st.text, ()), (st.binary, ()), (st.builds, (lambda x, y: x + y, st.integers(), st.integers())), ) def test_produces_valid_examples_from_args(fn, args): check_can_generate_examples(fn(*args)) def test_build_class_with_target_kwarg(): NamedTupleWithTargetField = collections.namedtuple("Something", ["target"]) check_can_generate_examples( st.builds(NamedTupleWithTargetField, target=st.integers()) ) def test_builds_raises_with_no_target(): with pytest.raises(TypeError): check_can_generate_examples(st.builds()) @pytest.mark.parametrize("non_callable", [1, "abc", st.integers()]) def test_builds_raises_if_non_callable_as_target_kwarg(non_callable): with pytest.raises(TypeError): check_can_generate_examples(st.builds(target=non_callable)) @pytest.mark.parametrize("non_callable", [1, "abc", st.integers()]) def test_builds_raises_if_non_callable_as_first_arg(non_callable): # If there are any positional arguments, then the target (which must be # callable) must be specified as the first one. with pytest.raises(InvalidArgument): check_can_generate_examples(st.builds(non_callable, target=lambda x: x)) def test_tuples_raise_error_on_bad_kwargs(): with pytest.raises(TypeError): st.tuples(stuff="things") @given(st.lists(st.booleans(), min_size=10, max_size=10)) def test_has_specified_length(xs): assert len(xs) == 10 @given(st.integers(max_value=100)) @settings(max_examples=100) def test_has_upper_bound(x): assert x <= 100 @given(st.integers(min_value=100)) def test_has_lower_bound(x): assert x >= 100 @given(st.integers(min_value=1, max_value=2)) def test_is_in_bounds(x): assert 1 <= x <= 2 @given(st.fractions(min_value=-1, max_value=1, max_denominator=1000)) def test_fraction_is_in_bounds(x): assert -1 <= x <= 1 assert abs(x.denominator) <= 1000 @given(st.fractions(min_value=fractions.Fraction(1, 2))) def test_fraction_gt_positive(x): assert fractions.Fraction(1, 2) <= x @given(st.fractions(max_value=fractions.Fraction(-1, 2))) def test_fraction_lt_negative(x): assert x <= fractions.Fraction(-1, 2) @given(st.decimals(min_value=-1.5, max_value=1.5)) def test_decimal_is_in_bounds(x): assert decimal.Decimal("-1.5") <= x <= decimal.Decimal("1.5") def test_float_can_find_max_value_inf(): assert minimal(st.floats(max_value=math.inf), math.isinf) == float("inf") assert minimal(st.floats(min_value=0.0), math.isinf) == math.inf def test_float_can_find_min_value_inf(): minimal(st.floats(), lambda x: x < 0 and math.isinf(x)) minimal(st.floats(min_value=-math.inf, max_value=0.0), math.isinf) def test_can_find_none_list(): assert minimal(st.lists(st.none()), lambda x: len(x) >= 3) == [None] * 3 def test_fractions(): assert minimal(st.fractions(), lambda f: f >= 1) == 1 def test_decimals(): assert minimal(st.decimals(), lambda f: f.is_finite() and f >= 1) == 1 @xfail_on_crosshair( Why.undiscovered ) # (SampledFromStrategy.calc_label() hashes a symbolic int) def test_non_float_decimal(): minimal(st.decimals(), lambda d: d.is_finite() and decimal.Decimal(float(d)) != d) def test_produces_dictionaries_of_at_least_minimum_size(): t = minimal( st.dictionaries(st.booleans(), st.integers(), min_size=2), ) assert t == {False: 0, True: 0} @given(st.dictionaries(st.integers(), st.integers(), max_size=5)) @settings(max_examples=50) def test_dictionaries_respect_size(d): assert len(d) <= 5 @given(st.dictionaries(st.integers(), st.integers(), max_size=0)) @settings(max_examples=50) def test_dictionaries_respect_zero_size(d): assert len(d) <= 5 @given(st.lists(st.none(), max_size=5)) def test_none_lists_respect_max_size(ls): assert len(ls) <= 5 @given(st.lists(st.none(), max_size=5, min_size=1)) def test_none_lists_respect_max_and_min_size(ls): assert 1 <= len(ls) <= 5 @given(st.iterables(st.integers(), max_size=5, min_size=1)) def test_iterables_are_exhaustible(it): for _ in it: pass with pytest.raises(StopIteration): next(it) def test_minimal_iterable(): assert list(minimal(st.iterables(st.integers()))) == [] @pytest.mark.parametrize("parameter_name", ["min_value", "max_value"]) @pytest.mark.parametrize("value", [-1, 0, 1]) def test_no_infinity_for_min_max_values(value, parameter_name): kwargs = {"allow_infinity": False, parameter_name: value} @given(st.floats(**kwargs)) def test_not_infinite(xs): assert not math.isinf(xs) test_not_infinite() @pytest.mark.parametrize("parameter_name", ["min_value", "max_value"]) @pytest.mark.parametrize("value", [-1, 0, 1]) def test_no_nan_for_min_max_values(value, parameter_name): kwargs = {"allow_nan": False, parameter_name: value} @given(st.floats(**kwargs)) def test_not_nan(xs): assert not math.isnan(xs) test_not_nan() class Sneaky: """It's like a strategy, but it's not a strategy.""" is_empty = False depth = 0 label = 0 def do_draw(self, data): pass def validate(self): pass @pytest.mark.parametrize("value", [5, Sneaky()]) @pytest.mark.parametrize("label", [None, "not a strategy"]) @given(data=st.data()) def test_data_explicitly_rejects_non_strategies(data, value, label): with pytest.raises(InvalidArgument): data.draw(value, label=label) @given(st.integers().filter(bool).filter(lambda x: x % 3)) def test_chained_filter(x): assert x assert x % 3 def test_chained_filter_tracks_all_conditions(): s = st.integers().filter(bool).filter(lambda x: x % 3) assert len(s.wrapped_strategy.flat_conditions) == 2 @pytest.mark.parametrize("version", [4, 6]) @given(data=st.data()) def test_ipaddress_from_network_is_always_correct_version(data, version): ip = data.draw(st.ip_addresses(v=version), label="address") assert ip.version == version @given(data=st.data(), network=st.from_type(IPv4Network) | st.from_type(IPv6Network)) def test_ipaddress_from_network_is_always_in_network(data, network): ip = data.draw(st.ip_addresses(network=network), label="address") assert ip in network assert ip.version == network.version class AnEnum(enum.Enum): a = 1 def requires_arg(value): """Similar to the enum.Enum.__call__ method.""" @given(st.data()) def test_builds_error_messages(data): # If we call them directly, we get a simple TypeError in both cases with pytest.raises(TypeError): requires_arg() with pytest.raises(TypeError): AnEnum() # But we have an improved error message if you try to build an Enum assert issubclass(InvalidArgument, TypeError) # it's a valid substitution with pytest.raises(TypeError): # which only applies to enums data.draw(st.builds(requires_arg)) with pytest.raises( InvalidArgument, match=r".* try using sampled_from\(.+\) instead of builds\(.+\)", ): data.draw(st.builds(AnEnum)) # and sampled_from() does in fact work data.draw(st.sampled_from(AnEnum)) @pytest.mark.parametrize( "strat_a,strat_b", [ pytest.param( st.integers(), st.integers(0), marks=pytest.mark.xfail( # this is the exception raised by failed pytest.warns(), # ref https://github.com/pytest-dev/pytest/issues/8928 raises=pytest.fail.Exception, strict=True, reason="constraints not checked", ), ), (st.builds(int), st.builds(float)), (st.none(), st.integers()), ( st.composite(lambda draw: draw(st.none()))(), st.composite(lambda draw: draw(st.integers()))(), ), ], ) def test_incompatible_shared_strategies_warns(strat_a, strat_b): shared_a = st.shared(strat_a, key="share") shared_b = st.shared(strat_b, key="share") @given(shared_a, shared_b) @settings(max_examples=10, phases=[Phase.generate]) def test_it(a, b): assert a == b with pytest.warns(HypothesisWarning, match="Different strategies"): test_it() @st.composite def _composite1(draw): return draw(st.integers()) @st.composite def _composite2(draw): return draw(st.integers()) @pytest.mark.parametrize( "strat_a,strat_b", [ (st.floats(allow_nan=False), st.floats(allow_nan=False)), (st.builds(float), st.builds(float)), (_composite1(), _composite1()), ( st.floats(allow_nan=False, allow_infinity=False), st.floats(allow_nan=False, allow_infinity=0), ), (_composite1(), _composite2()), pytest.param( st.integers().flatmap(st.just), st.integers(), marks=pytest.mark.xfail( raises=HypothesisWarning, strict=True, reason="really different (but compatible)", ), ), ], ) def test_compatible_shared_strategies_do_not_warn(strat_a, strat_b): shared_a = st.shared(strat_a, key="share") shared_b = st.shared(strat_b, key="share") @given(shared_a, shared_b) @settings(max_examples=10, phases=[Phase.generate]) def test_it(a, b): assert a == b with warnings.catch_warnings(): warnings.simplefilter("error", HypothesisWarning) test_it() def test_compatible_nested_shared_strategies_do_not_warn(): shared_a = st.shared(st.integers(), key="share") shared_b = st.shared(st.integers(), key="share") shared_c = st.shared(shared_a, key="nested_share") shared_d = st.shared(shared_b, key="nested_share") @given(shared_a, shared_b, shared_c, shared_d) @settings(max_examples=10, phases=[Phase.generate]) def test_it(a, b, c, d): assert a == b == c == d test_it() ================================================ FILE: hypothesis-python/tests/cover/test_draw_example.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis.strategies import lists from tests.common import standard_types from tests.common.debug import check_can_generate_examples @pytest.mark.parametrize("spec", standard_types, ids=repr) def test_single_example(spec): check_can_generate_examples(spec) @pytest.mark.parametrize("spec", standard_types, ids=repr) def test_list_example(spec): check_can_generate_examples(lists(spec)) ================================================ FILE: hypothesis-python/tests/cover/test_error_in_draw.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import pytest from hypothesis import given, strategies as st from hypothesis.errors import HypothesisWarning from hypothesis.stateful import RuleBasedStateMachine, rule, run_state_machine_as_test def test_error_is_in_finally(): @given(st.data()) def test(d): try: d.draw(st.lists(st.integers(), min_size=3, unique=True)) finally: raise ValueError with pytest.raises(ValueError) as err: test() assert "[0, 1, -1]" in "\n".join(err.value.__notes__) @given(st.data()) def test_warns_on_bool_strategy(data): with pytest.warns( HypothesisWarning, match=r"bool\(.+\) is always True, did you mean to draw a value\?", ): if st.booleans(): # 'forgot' to draw from the strategy pass def test_adds_note_showing_which_strategy(): class X: def __init__(self, y: int) -> None: assert y == 7 @given(...) def inner(value: X): assert isinstance(value, X) rep = re.escape(repr(st.from_type(X))) with pytest.raises(AssertionError, match=f".*while generating 'value' from {rep}"): inner() def test_adds_note_showing_which_strategy_stateful(): strategy = st.integers().map(lambda x: x / 0) class Machine(RuleBasedStateMachine): @rule(value=strategy) def take_a_step(self, value): assert value msg = f"while generating 'value' from {strategy!r} for rule take_a_step" print(msg) with pytest.raises(ZeroDivisionError, match=f".*{re.escape(msg)}"): run_state_machine_as_test(Machine) ================================================ FILE: hypothesis-python/tests/cover/test_escalation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import pytest import hypothesis from hypothesis import errors from hypothesis.internal import escalation as esc from hypothesis.internal.compat import BaseExceptionGroup def test_is_hypothesis_file_not_confused_by_prefix(monkeypatch): # Errors in third-party extensions such as `hypothesis-trio` or # `hypothesis-jsonschema` used to be incorrectly considered to be # Hypothesis internal errors, which could result in confusing error # messages. This test makes sure that files like: # `[...]/python3.12/site-packages/hypothesis_something/[...]` # are not considered as hypothesis files. root = os.path.dirname(hypothesis.__file__) assert esc.is_hypothesis_file(hypothesis.__file__) assert esc.is_hypothesis_file(esc.__file__) assert not esc.is_hypothesis_file(pytest.__file__) assert not esc.is_hypothesis_file(root + "-suffix") assert not esc.is_hypothesis_file(root + "-suffix/something.py") @pytest.mark.parametrize("fname", ["", ""]) def test_is_hypothesis_file_does_not_error_on_invalid_paths_issue_2319(fname): assert not esc.is_hypothesis_file(fname) def test_multiplefailures_deprecation(): with pytest.warns(errors.HypothesisDeprecationWarning): exc = errors.MultipleFailures assert exc is BaseExceptionGroup def test_errors_attribute_error(): with pytest.raises(AttributeError): errors.ThisIsNotARealAttributeDontCreateSomethingWithThisName def test_handles_null_traceback(): esc.InterestingOrigin.from_exception(Exception()) def test_handles_context(): e = ValueError() e.__context__ = KeyError() origin = esc.InterestingOrigin.from_exception(e) assert "ValueError at " in str(origin) assert " context: " in str(origin) assert "KeyError at " in str(origin) def test_handles_groups(): origin = esc.InterestingOrigin.from_exception( BaseExceptionGroup("message", [ValueError("msg2")]) ) assert "ExceptionGroup at " in str(origin) assert "child exception" in str(origin) assert "ValueError at " in str(origin) def make_exceptions_with_cycles(): err = ValueError() err.__context__ = err yield err err = TypeError() err.__context__ = BaseExceptionGroup("msg", [err]) yield err inner = LookupError() err = BaseExceptionGroup("msg", [inner]) inner.__context__ = err yield err inner = OSError() yield BaseExceptionGroup("msg", [inner, inner, inner]) @pytest.mark.parametrize("err", list(make_exceptions_with_cycles())) def test_handles_cycles(err): esc.InterestingOrigin.from_exception(err) ================================================ FILE: hypothesis-python/tests/cover/test_example.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import example, given, strategies as st from hypothesis.errors import InvalidArgument from tests.common.utils import fails_with @example(False).via("Manually specified") @given(st.booleans()) def test_ok_example_via(x): pass def test_invalid_example_via(): with pytest.raises(InvalidArgument): example(x=False).via(100) # not a string! with pytest.raises(TypeError): example(x=False).via("abc", "def") # too many args @pytest.mark.parametrize( "kw", [ {"condition": None}, # must be a bool {"reason": None}, # must be a string {"raises": None}, # not a BaseException (or even a type) {"raises": int}, # not a BaseException {"raises": [Exception]}, # not a tuple {"raises": (None,)}, # tuple containing a non-BaseException {"raises": ()}, # empty tuple doesn't make sense here # raising non-failure exceptions, eg KeyboardInterrupt, is tested below ], ids=repr, ) def test_invalid_example_xfail_arguments(kw): with pytest.raises(InvalidArgument): example(x=False).xfail(**kw) @example(True).xfail() @example(True).xfail(reason="ignored for passing tests") @example(True).xfail(raises=KeyError) @example(True).xfail(raises=(KeyError, ValueError)) @example(True).xfail(True, reason="...") @example(False).xfail(condition=False) @given(st.none()) def test_many_xfail_example_decorators(fails): if fails: raise KeyError @fails_with(AssertionError) @example(x=True).xfail(raises=KeyError) @given(st.none()) def test_xfail_reraises_non_specified_exception(x): assert not x @fails_with( InvalidArgument, match=r"@example\(x=True\) raised an expected BaseException\('msg'\), " r"but Hypothesis does not treat this as a test failure", ) @example(True).xfail() @given(st.none()) def test_must_raise_a_failure_exception(x): if x: raise BaseException("msg") @fails_with( AssertionError, match=r"Expected an exception from @example\(x=None\), but no exception was raised.", ) @example(None).xfail() @given(st.none()) def test_error_on_unexpected_pass_base(x): pass @fails_with( AssertionError, match=r"Expected an AssertionError from @example\(x=None\), but no exception was raised.", ) @example(None).xfail(raises=AssertionError) @given(st.none()) def test_error_on_unexpected_pass_single(x): pass @fails_with( AssertionError, match=r"Expected an AssertionError from @example\(x=None\), but no exception was raised.", ) @example(None).xfail(raises=(AssertionError,)) @given(st.none()) def test_error_on_unexpected_pass_single_elem_tuple(x): pass @fails_with( AssertionError, match=r"Expected a KeyError, or ValueError from @example\(x=None\), but no exception was raised.", ) @example(None).xfail(raises=(KeyError, ValueError)) @given(st.none()) def test_error_on_unexpected_pass_multi(x): pass ================================================ FILE: hypothesis-python/tests/cover/test_exceptiongroup.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from contextlib import suppress import pytest from hypothesis import given, strategies as st from hypothesis.errors import Flaky, FlakyBackendFailure, FlakyFailure, Frozen, StopTest from hypothesis.internal.compat import BaseExceptionGroup, ExceptionGroup from hypothesis.strategies import DataObject def test_discard_frozen() -> None: @given(st.data()) def discard_frozen(data: DataObject) -> None: # Accessing .conjecture_data is internal API. Other possible ways of freezing # data might go through ConjectureRunner.cached_test_function_ir or # ConjectureRunner.test_function data.conjecture_data.freeze() # Raising Frozen doesn't actually do anything, what matters is # whether the data is frozen. raise ExceptionGroup("", [Frozen()]) discard_frozen() def test_discard_multiple_frozen() -> None: @given(st.data()) def discard_multiple_frozen(data: DataObject) -> None: data.conjecture_data.freeze() raise ExceptionGroup("", [Frozen(), Frozen()]) discard_multiple_frozen() def test_user_error_and_frozen() -> None: @given(st.data()) def user_error_and_frozen(data: DataObject) -> None: raise ExceptionGroup("", [Frozen(), TypeError()]) with pytest.raises(ExceptionGroup) as excinfo: user_error_and_frozen() e = excinfo.value assert isinstance(e, ExceptionGroup) assert len(e.exceptions) == 2 assert isinstance(e.exceptions[0], Frozen) assert isinstance(e.exceptions[1], TypeError) def test_user_error_and_stoptest() -> None: # if the code base had "proper" handling of exceptiongroups, the StopTest would # probably be handled by an except*. # TODO: which I suppose is an argument in favor of stripping it?? @given(st.data()) def user_error_and_stoptest(data: DataObject) -> None: raise BaseExceptionGroup( "", [StopTest(data.conjecture_data.testcounter), TypeError()] ) with pytest.raises(BaseExceptionGroup) as excinfo: user_error_and_stoptest() e = excinfo.value assert isinstance(e, BaseExceptionGroup) assert len(e.exceptions) == 2 assert isinstance(e.exceptions[0], StopTest) assert isinstance(e.exceptions[1], TypeError) def test_lone_user_error() -> None: # we don't want to unwrap exceptiongroups, since they might contain # useful debugging info @given(st.data()) def lone_user_error(data: DataObject) -> None: raise ExceptionGroup("foo", [TypeError()]) with pytest.raises(ExceptionGroup) as excinfo: lone_user_error() e = excinfo.value assert isinstance(e, ExceptionGroup) assert len(e.exceptions) == 1 assert isinstance(e.exceptions[0], TypeError) def test_nested_stoptest() -> None: @given(st.data()) def nested_stoptest(data: DataObject) -> None: raise BaseExceptionGroup( "", [BaseExceptionGroup("", [StopTest(data.conjecture_data.testcounter)])], ) nested_stoptest() def test_frozen_and_stoptest() -> None: # frozen+stoptest => strip frozen and let engine handle StopTest # actually.. I don't think I've got a live repo for this either. @given(st.data()) def frozen_and_stoptest(data: DataObject) -> None: raise BaseExceptionGroup( "", [StopTest(data.conjecture_data.testcounter), Frozen()] ) frozen_and_stoptest() def test_multiple_stoptest_1() -> None: # multiple stoptest, reraise the one with lowest testcounter @given(st.data()) def multiple_stoptest(data: DataObject) -> None: c = data.conjecture_data.testcounter raise BaseExceptionGroup("", [StopTest(c), StopTest(c + 1)]) multiple_stoptest() def test_multiple_stoptest_2() -> None: # the lower value is raised, which does not match data.conjecture_data.testcounter # so it is not handled by the engine @given(st.data()) def multiple_stoptest_2(data: DataObject) -> None: c = data.conjecture_data.testcounter raise BaseExceptionGroup("", [StopTest(c), StopTest(c - 1)]) with pytest.raises(StopTest): multiple_stoptest_2() def test_stoptest_and_hypothesisexception() -> None: # current code raises the first hypothesisexception and throws away stoptest @given(st.data()) def stoptest_and_hypothesisexception(data: DataObject) -> None: c = data.conjecture_data.testcounter raise BaseExceptionGroup("", [StopTest(c), Flaky()]) with pytest.raises(Flaky): stoptest_and_hypothesisexception() def test_multiple_hypothesisexception() -> None: # this can happen in several ways, see nocover/test_exceptiongroup.py @given(st.data()) def stoptest_and_hypothesisexception(data: DataObject) -> None: c = data.conjecture_data.testcounter raise BaseExceptionGroup("", [StopTest(c), Flaky()]) with pytest.raises(Flaky): stoptest_and_hypothesisexception() @pytest.mark.parametrize("ExceptionClass", [FlakyFailure, FlakyBackendFailure]) def test_exceptiongroups_reconstruct_original_type(ExceptionClass): # contextlib.suppress uses .split, and if ExceptionClass does not implement # .derive, .split will return a standard ExceptionGroup instead of # ExceptionClass. # see https://github.com/python/cpython/issues/119287 class UnrelatedException(Exception): pass with pytest.raises(ExceptionClass), suppress(UnrelatedException): raise ExceptionClass("exception message", [Exception()]) @pytest.mark.parametrize("ExceptionClass", [FlakyFailure, FlakyBackendFailure]) def test_derived_exception_group(ExceptionClass): exception = ExceptionClass("exception message", [Exception()]) # we don't expect a match, we just want to test .derive. match, rest = exception.split(()) assert match is None assert isinstance(rest, ExceptionClass) ================================================ FILE: hypothesis-python/tests/cover/test_executors.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import inspect from unittest import TestCase import pytest from hypothesis import example, given from hypothesis.strategies import booleans, integers def test_must_use_result_of_test(): class DoubleRun: def execute_example(self, function): x = function() if inspect.isfunction(x): return x() @given(booleans()) def boom(self, b): def f(): raise ValueError return f with pytest.raises(ValueError): DoubleRun().boom() class TestTryReallyHard(TestCase): @given(integers()) def test_something(self, i): pass def execute_example(self, f): f() return f() class Valueless: def execute_example(self, f): try: return f() except ValueError: return None @given(integers()) @example(1) def test_no_boom_on_example(self, x): raise ValueError @given(integers()) def test_no_boom(self, x): raise ValueError @given(integers()) def test_boom(self, x): raise AssertionError def test_boom(): with pytest.raises(AssertionError): Valueless().test_boom() def test_no_boom(): Valueless().test_no_boom() def test_no_boom_on_example(): Valueless().test_no_boom_on_example() ================================================ FILE: hypothesis-python/tests/cover/test_explicit_examples.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import time from unittest import TestCase import pytest from hypothesis import ( Phase, Verbosity, assume, example, given, note, reporting, settings, ) from hypothesis.errors import DeadlineExceeded, HypothesisWarning, InvalidArgument from hypothesis.internal.compat import ExceptionGroup from hypothesis.strategies import floats, integers, text from tests.common.utils import ( assert_falsifying_output, capture_out, fails_with, skipif_threading, ) class TestInstanceMethods(TestCase): @given(integers()) @example(1) def test_hi_1(self, x): assert isinstance(x, int) @given(integers()) @example(x=1) def test_hi_2(self, x): assert isinstance(x, int) @given(x=integers()) @example(x=1) def test_hi_3(self, x): assert isinstance(x, int) def test_kwarg_example_on_testcase(): class Stuff(TestCase): @given(integers()) @example(x=1) def test_hi(self, x): assert isinstance(x, int) Stuff("test_hi").test_hi() def test_errors_when_run_with_not_enough_args(): @given(integers(), int) @example(1) def foo(x, y): pass with pytest.raises(TypeError): foo() def test_errors_when_run_with_not_enough_kwargs(): @given(integers(), int) @example(x=1) def foo(x, y): pass with pytest.raises(TypeError): foo() def test_can_use_examples_after_given(): long_str = "This is a very long string that you've no chance of hitting" @example(long_str) @given(text()) def test_not_long_str(x): assert x != long_str with pytest.raises(AssertionError): test_not_long_str() def test_can_use_examples_before_given(): long_str = "This is a very long string that you've no chance of hitting" @given(text()) @example(long_str) def test_not_long_str(x): assert x != long_str with pytest.raises(AssertionError): test_not_long_str() def test_can_use_examples_around_given(): long_str = "This is a very long string that you've no chance of hitting" short_str = "Still no chance" seen = [] @example(short_str) @given(text()) @example(long_str) def test_not_long_str(x): seen.append(x) test_not_long_str() assert set(seen[:2]) == {long_str, short_str} @pytest.mark.parametrize(("x", "y"), [(1, False), (2, True)]) @example(z=10) @given(z=integers()) def test_is_a_thing(x, y, z): pass def test_no_args_and_kwargs(): with pytest.raises(InvalidArgument): example(1, y=2) def test_no_empty_examples(): with pytest.raises(InvalidArgument): example() def test_does_not_print_on_explicit_examples_if_no_failure(): @example(1) @given(integers()) def test_positive(x): assert x > 0 with ( reporting.with_reporter(reporting.default), pytest.raises(AssertionError), capture_out() as out, ): test_positive() out = out.getvalue() assert "Falsifying example: test_positive(1)" not in out def test_prints_output_for_explicit_examples(): @example(-1) @given(integers()) def test_positive(x): assert x > 0 assert_falsifying_output(test_positive, "Falsifying explicit", x=-1) def test_prints_verbose_output_for_explicit_examples(): @settings(verbosity=Verbosity.verbose) @example("NOT AN INTEGER") @given(integers()) def test_always_passes(x): pass assert_falsifying_output( test_always_passes, expected_exception=None, example_type="Trying explicit", x="NOT AN INTEGER", ) def test_captures_original_repr_of_example(): @example(x=[]) @given(integers()) def test_mutation(x): x.append(1) assert not x assert_falsifying_output(test_mutation, "Falsifying explicit", x=[]) def test_examples_are_tried_in_order(): @example(x=1) @example(x=2) @given(integers()) @settings(phases=[Phase.explicit]) @example(x=3) def test(x): print(f"x -> {x}") with capture_out() as out, reporting.with_reporter(reporting.default): test() ls = out.getvalue().splitlines() assert ls == ["x -> 1", "x -> 2", "x -> 3"] def test_prints_note_in_failing_example(): @example(x=42) @example(x=43) @given(integers()) def test(x): note(f"x -> {x}") assert x == 42 with pytest.raises(AssertionError) as err: test() assert "x -> 43" in err.value.__notes__ assert "x -> 42" not in err.value.__notes__ def test_must_agree_with_number_of_arguments(): @example(1, 2) @given(integers()) def test(a): pass with pytest.raises(InvalidArgument): test() @skipif_threading # deadline disabled under threading @fails_with(DeadlineExceeded) @example(10) @settings(phases=[Phase.explicit], deadline=1) @given(integers()) def test(x): time.sleep(10) @given(value=floats(0, 1)) @example(value=0.56789) @pytest.mark.parametrize("threshold", [0.5, 1]) def test_unsatisfied_assumption_during_explicit_example(threshold, value): # Regression test, expected to pass / skip depending on parametrize. # See https://github.com/HypothesisWorks/hypothesis/issues/2125 assume(value < threshold) @pytest.mark.parametrize("exc", [ExceptionGroup, AssertionError]) def test_multiple_example_reporting(exc): @example(1) @example(2) @settings(report_multiple_bugs=exc is ExceptionGroup, phases=[Phase.explicit]) @given(integers()) def inner_test_multiple_failing_examples(x): assert x < 2 assert x < 1 with pytest.raises(exc): inner_test_multiple_failing_examples() def test_simplifies_multiple_examples_with_same_error(): # When multiple examples fail with the same error, only the simplest is shown @example(x=1000) @example(x=1) @example(x=100) @settings(report_multiple_bugs=True, phases=[Phase.explicit]) @given(x=integers()) def test_fn(x): assert x < 0 with pytest.raises(AssertionError) as exc_info: test_fn() err = exc_info.value # Should show note about other examples assert any( "2 other explicit examples also failed" in note for note in err.__notes__ ) # Should show the simplest example (x=1 is shorter than x=100 or x=1000) assert any("x=1," in note for note in err.__notes__) def test_shows_all_examples_at_verbose(): # At verbose, all examples with same error should be shown @example(x=1000) @example(x=1) @settings( report_multiple_bugs=True, phases=[Phase.explicit], verbosity=Verbosity.verbose ) @given(x=integers()) def test_fn(x): assert x < 0 with pytest.raises(ExceptionGroup) as exc_info: test_fn() group = exc_info.value # Both examples should be shown assert len(group.exceptions) == 2 # No simplification note should be present for exc in group.exceptions: assert not any( "other explicit example" in note for note in getattr(exc, "__notes__", []) ) def test_different_errors_not_simplified(): # Examples with different errors should all be shown @example(x=1) @example(x=2) @settings(report_multiple_bugs=True, phases=[Phase.explicit]) @given(x=integers()) def test_fn(x): if x == 1: raise ValueError("one") raise TypeError("two") with pytest.raises(ExceptionGroup) as exc_info: test_fn() assert {type(e) for e in exc_info.value.exceptions} == {ValueError, TypeError} @example(text()) @given(text()) def test_example_decorator_accepts_strategies(s): """The custom error message only happens when the test has already failed.""" def test_helpful_message_when_example_fails_because_it_was_passed_a_strategy(): @example(text()) @given(text()) def t(s): assert isinstance(s, str) try: t() except HypothesisWarning as err: assert isinstance(err.__cause__, AssertionError) else: raise NotImplementedError("should be unreachable") def test_stop_silently_dropping_examples_when_decorator_is_applied_to_itself(): def f(): pass test = example("outer")(example("inner"))(f) assert len(test.hypothesis_explicit_examples) == 2 ================================================ FILE: hypothesis-python/tests/cover/test_falsifying_example_output.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import Phase, example, given, settings, strategies as st OUTPUT_WITH_BREAK = """ Falsifying explicit example: test( x={0!r}, y={0!r}, ) """ @pytest.mark.parametrize("n", [10, 100]) def test_inserts_line_breaks_only_at_appropriate_lengths(n): @example("0" * n, "0" * n) @given(st.text(), st.text()) def test(x, y): assert x < y with pytest.raises(AssertionError) as err: test() assert OUTPUT_WITH_BREAK.format("0" * n).strip() == "\n".join(err.value.__notes__) @given(kw=st.none()) def generate_phase(*args, kw): assert args != (1, 2, 3) @given(kw=st.none()) @example(kw=None) @settings(phases=[Phase.explicit]) def explicit_phase(*args, kw): assert args != (1, 2, 3) @pytest.mark.parametrize( "fn", [generate_phase, explicit_phase], ids=lambda fn: fn.__name__, ) def test_vararg_output(fn): with pytest.raises(AssertionError) as err: fn(1, 2, 3) assert "1,\n 2,\n 3,\n" in "\n".join(err.value.__notes__) ================================================ FILE: hypothesis-python/tests/cover/test_feature_flags.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st from hypothesis.strategies._internal.featureflags import FeatureFlags, FeatureStrategy from tests.common.debug import find_any, minimal STRAT = FeatureStrategy() def test_can_all_be_enabled(): find_any(STRAT, lambda x: all(x.is_enabled(i) for i in range(100))) def test_minimizes_open(): features = range(10) flags = minimal(STRAT, lambda x: [x.is_enabled(i) for i in features]) assert all(flags.is_enabled(i) for i in features) def test_minimizes_individual_features_to_open(): features = list(range(10)) flags = minimal( STRAT, lambda x: sum(x.is_enabled(i) for i in features) < len(features) ) assert all(flags.is_enabled(i) for i in features[:-1]) assert not flags.is_enabled(features[-1]) def test_marks_unknown_features_as_enabled(): x = find_any(STRAT, lambda v: True) assert x.is_enabled("fish") def test_by_default_all_enabled(): f = FeatureFlags() assert f.is_enabled("foo") def test_eval_featureflags_repr(): flags = FeatureFlags(enabled=["on"], disabled=["off"]) assert flags.is_enabled("on") assert not flags.is_enabled("off") flags2 = eval(repr(flags)) assert flags2.is_enabled("on") assert not flags2.is_enabled("off") @given(st.data()) def test_repr_can_be_evalled(data): flags = data.draw(STRAT) features = data.draw(st.lists(st.text(), unique=True)) for f in features: flags.is_enabled(f) flags2 = eval(repr(flags)) for f in features: assert flags2.is_enabled(f) == flags.is_enabled(f) more_features = data.draw(st.lists(st.text().filter(lambda s: s not in features))) for f in more_features: assert flags2.is_enabled(f) @given(FeatureStrategy(at_least_one_of={"a", "b", "c"})) def test_can_avoid_disabling_every_flag(flags): assert any(flags.is_enabled(k) for k in {"a", "b", "c"}) ================================================ FILE: hypothesis-python/tests/cover/test_filestorage.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os from hypothesis import configuration as fs previous_home_dir = None def setup_function(function): global previous_home_dir previous_home_dir = fs.storage_directory() fs.set_hypothesis_home_dir(None) def teardown_function(function): global previous_home_dir fs.set_hypothesis_home_dir(previous_home_dir) previous_home_dir = None def test_defaults_to_the_default(): assert fs.storage_directory() == fs.__hypothesis_home_directory_default def test_can_set_homedir(tmp_path): fs.set_hypothesis_home_dir(tmp_path) assert fs.storage_directory("kittens") == tmp_path / "kittens" def test_will_pick_up_location_from_env(monkeypatch, tmp_path): monkeypatch.setattr(os, "environ", {"HYPOTHESIS_STORAGE_DIRECTORY": str(tmp_path)}) assert fs.storage_directory() == tmp_path def test_storage_directories_are_not_created_automatically(tmp_path): fs.set_hypothesis_home_dir(tmp_path) assert not fs.storage_directory("badgers").exists() ================================================ FILE: hypothesis-python/tests/cover/test_filter_rewriting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime as dt import decimal import math import operator import re import sys from fractions import Fraction from functools import partial from sys import float_info import pytest from hypothesis import HealthCheck, given, settings, strategies as st from hypothesis.errors import HypothesisWarning, Unsatisfiable from hypothesis.internal.conjecture.providers import COLLECTION_DEFAULT_MAX_SIZE from hypothesis.internal.filtering import max_len, min_len from hypothesis.internal.floats import next_down, next_up from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.core import data from hypothesis.strategies._internal.lazy import LazyStrategy, unwrap_strategies from hypothesis.strategies._internal.numbers import FloatStrategy, IntegersStrategy from hypothesis.strategies._internal.strategies import FilteredStrategy, MappedStrategy from hypothesis.strategies._internal.strings import BytesStrategy, TextStrategy from tests.common.debug import check_can_generate_examples from tests.common.utils import fails_with A_FEW = 15 # speed up massively-parametrized tests @pytest.mark.parametrize( "strategy, predicate, start, end", [ # Finitude check (st.integers(1, 5), math.isfinite, 1, 5), # Integers with integer bounds (st.integers(1, 5), partial(operator.lt, 3), 4, 5), # lambda x: 3 < x (st.integers(1, 5), partial(operator.le, 3), 3, 5), # lambda x: 3 <= x (st.integers(1, 5), partial(operator.eq, 3), 3, 3), # lambda x: 3 == x (st.integers(1, 5), partial(operator.ge, 3), 1, 3), # lambda x: 3 >= x (st.integers(1, 5), partial(operator.gt, 3), 1, 2), # lambda x: 3 > x # Integers with non-integer bounds (st.integers(1, 5), partial(operator.lt, 3.5), 4, 5), (st.integers(1, 5), partial(operator.le, 3.5), 4, 5), (st.integers(1, 5), partial(operator.ge, 3.5), 1, 3), (st.integers(1, 5), partial(operator.gt, 3.5), 1, 3), (st.integers(1, 5), partial(operator.lt, -math.inf), 1, 5), (st.integers(1, 5), partial(operator.gt, math.inf), 1, 5), # Integers with only one bound (st.integers(min_value=1), partial(operator.lt, 3), 4, None), (st.integers(min_value=1), partial(operator.le, 3), 3, None), (st.integers(max_value=5), partial(operator.ge, 3), None, 3), (st.integers(max_value=5), partial(operator.gt, 3), None, 2), # Unbounded integers (st.integers(), partial(operator.lt, 3), 4, None), (st.integers(), partial(operator.le, 3), 3, None), (st.integers(), partial(operator.eq, 3), 3, 3), (st.integers(), partial(operator.ge, 3), None, 3), (st.integers(), partial(operator.gt, 3), None, 2), # Simple lambdas (st.integers(), lambda x: x < 3, None, 2), (st.integers(), lambda x: x <= 3, None, 3), (st.integers(), lambda x: x == 3, 3, 3), (st.integers(), lambda x: x >= 3, 3, None), (st.integers(), lambda x: x > 3, 4, None), # Simple lambdas, reverse comparison (st.integers(), lambda x: 3 > x, None, 2), (st.integers(), lambda x: 3 >= x, None, 3), (st.integers(), lambda x: 3 == x, 3, 3), (st.integers(), lambda x: 3 <= x, 3, None), (st.integers(), lambda x: 3 < x, 4, None), # More complicated lambdas (st.integers(), lambda x: 0 < x < 5, 1, 4), (st.integers(), lambda x: 0 < x >= 1, 1, None), (st.integers(), lambda x: 1 > x <= 0, None, 0), (st.integers(), lambda x: x > 0 and x > 0, 1, None), (st.integers(), lambda x: x < 1 and x < 1, None, 0), (st.integers(), lambda x: x > 1 and x > 0, 2, None), (st.integers(), lambda x: x < 1 and x < 2, None, 0), ], ids=get_pretty_function_description, ) @settings(max_examples=A_FEW) @given(data=st.data()) def test_filter_rewriting_ints(data, strategy, predicate, start, end): s = strategy.filter(predicate) assert isinstance(s, LazyStrategy) assert isinstance(s.wrapped_strategy, IntegersStrategy) assert s.wrapped_strategy.start == start assert s.wrapped_strategy.end == end value = data.draw(s) assert predicate(value) @pytest.mark.parametrize( "strategy, predicate, min_value, max_value", [ # Floats with integer bounds (st.floats(1, 5), partial(operator.lt, 3), next_up(3.0), 5), # 3 < x (st.floats(1, 5), partial(operator.le, 3), 3, 5), # lambda x: 3 <= x (st.floats(1, 5), partial(operator.eq, 3), 3, 3), # lambda x: 3 == x (st.floats(1, 5), partial(operator.ge, 3), 1, 3), # lambda x: 3 >= x (st.floats(1, 5), partial(operator.gt, 3), 1, next_down(3.0)), # 3 > x # Floats with non-integer bounds (st.floats(1, 5), partial(operator.lt, 3.5), next_up(3.5), 5), (st.floats(1, 5), partial(operator.le, 3.5), 3.5, 5), (st.floats(1, 5), partial(operator.ge, 3.5), 1, 3.5), (st.floats(1, 5), partial(operator.gt, 3.5), 1, next_down(3.5)), (st.floats(1, 5), partial(operator.lt, -math.inf), 1, 5), (st.floats(1, 5), partial(operator.gt, math.inf), 1, 5), # Floats with only one bound (st.floats(min_value=1), partial(operator.lt, 3), next_up(3.0), math.inf), (st.floats(min_value=1), partial(operator.le, 3), 3, math.inf), (st.floats(max_value=5), partial(operator.ge, 3), -math.inf, 3), (st.floats(max_value=5), partial(operator.gt, 3), -math.inf, next_down(3.0)), # Unbounded floats (st.floats(), partial(operator.lt, 3), next_up(3.0), math.inf), (st.floats(), partial(operator.le, 3), 3, math.inf), (st.floats(), partial(operator.eq, 3), 3, 3), (st.floats(), partial(operator.ge, 3), -math.inf, 3), (st.floats(), partial(operator.gt, 3), -math.inf, next_down(3.0)), # Simple lambdas (st.floats(), lambda x: x < 3, -math.inf, next_down(3.0)), (st.floats(), lambda x: x <= 3, -math.inf, 3), (st.floats(), lambda x: x == 3, 3, 3), (st.floats(), lambda x: x >= 3, 3, math.inf), (st.floats(), lambda x: x > 3, next_up(3.0), math.inf), # Simple lambdas, reverse comparison (st.floats(), lambda x: 3 > x, -math.inf, next_down(3.0)), (st.floats(), lambda x: 3 >= x, -math.inf, 3), (st.floats(), lambda x: 3 == x, 3, 3), (st.floats(), lambda x: 3 <= x, 3, math.inf), (st.floats(), lambda x: 3 < x, next_up(3.0), math.inf), # More complicated lambdas (st.floats(), lambda x: 0 < x < 5, next_up(0.0), next_down(5.0)), (st.floats(), lambda x: 0 < x >= 1, 1, math.inf), (st.floats(), lambda x: 1 > x <= 0, -math.inf, 0), (st.floats(), lambda x: x > 0 and x > 0, next_up(0.0), math.inf), (st.floats(), lambda x: x < 1 and x < 1, -math.inf, next_down(1.0)), (st.floats(), lambda x: x > 1 and x > 0, next_up(1.0), math.inf), (st.floats(), lambda x: x < 1 and x < 2, -math.inf, next_down(1.0)), # Specific named functions (st.floats(), math.isfinite, next_up(-math.inf), next_down(math.inf)), ], ids=get_pretty_function_description, ) @settings(max_examples=A_FEW) @given(data=st.data()) def test_filter_rewriting_floats(data, strategy, predicate, min_value, max_value): s = strategy.filter(predicate) assert isinstance(s, LazyStrategy) assert isinstance(s.wrapped_strategy, FloatStrategy) assert s.wrapped_strategy.min_value == min_value assert s.wrapped_strategy.max_value == max_value value = data.draw(s) assert predicate(value) @pytest.mark.parametrize( "pred", [ math.isinf, math.isnan, partial(operator.lt, 6), partial(operator.eq, Fraction(10, 3)), partial(operator.ge, 0), partial(operator.lt, math.inf), partial(operator.gt, -math.inf), ], ) @pytest.mark.parametrize("s", [st.integers(1, 5), st.floats(1, 5)]) def test_rewrite_unsatisfiable_filter(s, pred): assert s.filter(pred).is_empty @pytest.mark.parametrize( "pred", [ partial(operator.eq, "numbers are never equal to strings"), ], ) @pytest.mark.parametrize("s", [st.integers(1, 5), st.floats(1, 5)]) @fails_with(Unsatisfiable) def test_erroring_rewrite_unsatisfiable_filter(s, pred): check_can_generate_examples(s.filter(pred)) @pytest.mark.parametrize( "strategy, predicate", [ (st.floats(), math.isinf), (st.floats(0, math.inf), math.isinf), (st.floats(), math.isnan), ], ) @given(data=st.data()) def test_misc_sat_filter_rewrites(data, strategy, predicate): s = strategy.filter(predicate).wrapped_strategy assert not isinstance(s, FloatStrategy) value = data.draw(s) assert predicate(value) @pytest.mark.parametrize( "strategy, predicate", [ (st.floats(allow_infinity=False), math.isinf), (st.floats(0, math.inf), math.isnan), (st.floats(allow_nan=False), math.isnan), ], ) @given(data=st.data()) def test_misc_unsat_filter_rewrites(data, strategy, predicate): assert strategy.filter(predicate).is_empty @given(st.integers(0, 2).filter(partial(operator.ne, 1))) def test_unhandled_operator(x): assert x in (0, 2) def test_rewriting_does_not_compare_decimal_snan(): s = st.integers(1, 5).filter(partial(operator.eq, decimal.Decimal("snan"))) s.wrapped_strategy with pytest.raises(decimal.InvalidOperation): check_can_generate_examples(s) @pytest.mark.parametrize("strategy", [st.integers(0, 1), st.floats(0, 1)], ids=repr) def test_applying_noop_filter_returns_self(strategy): s = strategy.wrapped_strategy s2 = s.filter(partial(operator.le, -1)).filter(partial(operator.ge, 2)) assert s is s2 def mod2(x): return x % 2 Y = 2**20 @pytest.mark.parametrize("s", [st.integers(1, 5), st.floats(1, 5)]) @given( data=st.data(), predicates=st.permutations( [ partial(operator.lt, 1), partial(operator.le, 2), partial(operator.ge, 4), partial(operator.gt, 5), mod2, lambda x: x > 2 or x % 7, lambda x: 0 < x <= Y, ] ), ) @settings(suppress_health_check=[HealthCheck.too_slow]) def test_rewrite_filter_chains_with_some_unhandled(data, predicates, s): # Set up our strategy for p in predicates: s = s.filter(p) # Whatever value we draw is in fact valid for these strategies value = data.draw(s) for p in predicates: assert p(value), f"{p=}, value={value}" # No matter the order of the filters, we get the same resulting structure unwrapped = s.wrapped_strategy assert isinstance(unwrapped, FilteredStrategy) assert isinstance(unwrapped.filtered_strategy, (IntegersStrategy, FloatStrategy)) for pred in unwrapped.flat_conditions: assert pred is mod2 or pred.__name__ == "" class NotAFunction: def __call__(self, bar): return True lambda_without_source = eval("lambda x: x > 2", {}, {}) assert get_pretty_function_description(lambda_without_source) == "lambda x: " @pytest.mark.parametrize( "start, end, predicate", [ (1, 4, lambda x: 0 < x < 5 and x % 7), (0, 9, lambda x: 0 <= x < 10 and x % 3), (1, None, lambda x: 0 < x <= Y), (None, None, lambda x: x == x), (None, None, lambda x: 1 == 1), (None, None, lambda x: 1 <= 2), (None, None, lambda x: x != 0), (None, None, NotAFunction()), (None, None, lambda_without_source), (None, None, lambda x, y=2: x >= 0), ], ) @given(data=st.data()) def test_rewriting_partially_understood_filters(data, start, end, predicate): s = st.integers().filter(predicate).wrapped_strategy assert isinstance(s, FilteredStrategy) assert isinstance(s.filtered_strategy, IntegersStrategy) assert s.filtered_strategy.start == start assert s.filtered_strategy.end == end assert s.flat_conditions == (predicate,) value = data.draw(s) assert predicate(value) @pytest.mark.parametrize( "strategy", [ st.text(), st.text(min_size=2), st.lists(st.none()), st.lists(st.none(), min_size=2), ], ) @pytest.mark.parametrize( "predicate", [bool, len, tuple, list, lambda x: x], ids=get_pretty_function_description, ) def test_sequence_filter_rewriting(strategy, predicate): s = unwrap_strategies(strategy) fs = s.filter(predicate) assert not isinstance(fs, FilteredStrategy) if s.min_size > 0: assert fs is s else: assert fs.min_size == 1 @pytest.mark.parametrize("method", [str.lower, str.title, str.upper]) def test_warns_on_suspicious_string_methods(method): s = unwrap_strategies(st.text()) with pytest.warns( HypothesisWarning, match="this allows all nonempty strings! Did you mean" ): fs = s.filter(method) assert fs.min_size == 1 @pytest.mark.parametrize("method", [str.isalnum]) def test_bumps_min_size_and_filters_for_content_str_methods(method): s = unwrap_strategies(st.text()) fs = s.filter(method) assert fs.filtered_strategy.min_size == 1 assert fs.flat_conditions == (method,) # Should we deterministically check whether ascii or not or st.characters fine? @pytest.mark.parametrize("al", [None, "cdef123", "cd12¥¦§©"]) @given(data()) def test_isidentifier_filter_properly_rewritten(al, data): if al is None: example = data.draw(st.text().filter(str.isidentifier)) else: example = data.draw(st.text(alphabet=al).filter(str.isidentifier)) assert set(example).issubset(al) assert example.isidentifier() def test_isidentifer_filter_unsatisfiable(): alphabet = "¥¦§©" assert not any(f"_{c}".isidentifier() for c in alphabet) fs = st.text(alphabet=alphabet).filter(str.isidentifier) with pytest.raises(Unsatisfiable): check_can_generate_examples(fs) @pytest.mark.parametrize( "op, attr, value, expected", [ (operator.lt, "min_value", -float_info.min / 2, 0), (operator.lt, "min_value", float_info.min / 2, float_info.min), (operator.gt, "max_value", float_info.min / 2, 0), (operator.gt, "max_value", -float_info.min / 2, -float_info.min), ], ) def test_filter_floats_can_skip_subnormals(op, attr, value, expected): base = st.floats(allow_subnormal=False).filter(partial(op, value)) assert getattr(base.wrapped_strategy, attr) == expected @pytest.mark.parametrize( "strategy, predicate, start, end", [ # text with integer bounds (st.text(min_size=1, max_size=5), partial(min_len, 3), 3, 5), (st.text(min_size=1, max_size=5), partial(max_len, 3), 1, 3), # text with only one bound (st.text(min_size=1), partial(min_len, 3), 3, math.inf), (st.text(min_size=1), partial(max_len, 3), 1, 3), (st.text(max_size=5), partial(min_len, 3), 3, 5), (st.text(max_size=5), partial(max_len, 3), 0, 3), # Unbounded text (st.text(), partial(min_len, 3), 3, math.inf), (st.text(), partial(max_len, 3), 0, 3), ], ids=get_pretty_function_description, ) @settings(max_examples=A_FEW) @given(data=st.data()) def test_filter_rewriting_text_partial_len(data, strategy, predicate, start, end): s = strategy.filter(predicate) assert isinstance(s, LazyStrategy) inner = unwrap_strategies(s) assert isinstance(inner, TextStrategy) assert inner.min_size == start assert inner.max_size == end value = data.draw(s) assert predicate(value) @given(data=st.data()) def test_can_rewrite_multiple_length_filters_if_not_lambdas(data): # This is a key capability for efficient rewriting using the `annotated-types` # package, although unfortunately we can't do it for lambdas. s = ( st.text(min_size=1, max_size=5) .filter(partial(min_len, 2)) .filter(partial(max_len, 4)) ) assert isinstance(s, LazyStrategy) inner = unwrap_strategies(s) assert isinstance(inner, TextStrategy) assert inner.min_size == 2 assert inner.max_size == 4 value = data.draw(s) assert 2 <= len(value) <= 4 @pytest.mark.parametrize( "predicate, start, end", [ # Simple lambdas (lambda x: len(x) < 3, 0, 2), (lambda x: len(x) <= 3, 0, 3), (lambda x: len(x) == 3, 3, 3), (lambda x: len(x) >= 3, 3, math.inf), (lambda x: len(x) > 3, 4, math.inf), # Simple lambdas, reverse comparison (lambda x: 3 > len(x), 0, 2), (lambda x: 3 >= len(x), 0, 3), (lambda x: 3 == len(x), 3, 3), (lambda x: 3 <= len(x), 3, math.inf), (lambda x: 3 < len(x), 4, math.inf), # More complicated lambdas (lambda x: 0 < len(x) < 5, 1, 4), (lambda x: 0 < len(x) >= 1, 1, math.inf), (lambda x: 1 > len(x) <= 0, 0, 0), (lambda x: len(x) > 0 and len(x) > 0, 1, math.inf), (lambda x: len(x) < 1 and len(x) < 1, 0, 0), (lambda x: len(x) > 1 and len(x) > 0, 2, math.inf), (lambda x: len(x) < 1 and len(x) < 2, 0, 0), ], ids=get_pretty_function_description, ) @pytest.mark.parametrize( "strategy", [ st.text(), st.lists(st.integers()), st.lists(st.integers(), unique=True), st.lists(st.sampled_from([1, 2, 3])), st.binary(), st.sets(st.integers()), st.frozensets(st.integers()), st.dictionaries(st.integers(), st.none()), st.lists(st.integers(), unique_by=lambda x: x % 17).map(tuple), ], ids=get_pretty_function_description, ) @settings(max_examples=A_FEW) @given(data=st.data()) def test_filter_rewriting_text_lambda_len(data, strategy, predicate, start, end): s = strategy.filter(predicate) unwrapped_nofilter = unwrap_strategies(strategy) unwrapped = unwrap_strategies(s) if was_mapped := isinstance(unwrapped, MappedStrategy): unwrapped = unwrapped.mapped_strategy assert isinstance(unwrapped, FilteredStrategy), f"{unwrapped=} {type(unwrapped)=}" assert isinstance( unwrapped.filtered_strategy, type(unwrapped_nofilter.mapped_strategy if was_mapped else unwrapped_nofilter), ) for pred in unwrapped.flat_conditions: assert pred.__name__ == "" if isinstance(unwrapped.filtered_strategy, MappedStrategy): unwrapped = unwrapped.filtered_strategy.mapped_strategy # binary() has a finite-but-effectively-infinite cap instead. if isinstance(unwrapped_nofilter, BytesStrategy) and end == math.inf: end = COLLECTION_DEFAULT_MAX_SIZE assert unwrapped.filtered_strategy.min_size == start assert unwrapped.filtered_strategy.max_size == end value = data.draw(s) assert predicate(value) two = 2 @pytest.mark.parametrize( "predicate, start, end", [ # Simple lambdas (lambda x: len(x) < 3, 0, 2), (lambda x: len(x) <= 3, 0, 3), (lambda x: len(x) == 3, 3, 3), (lambda x: len(x) >= 3, 3, 3), # input max element_count=3 # Simple lambdas, reverse comparison (lambda x: 3 > len(x), 0, 2), (lambda x: 3 >= len(x), 0, 3), (lambda x: 3 == len(x), 3, 3), (lambda x: 3 <= len(x), 3, 3), # input max element_count=3 # More complicated lambdas (lambda x: 0 < len(x) < 5, 1, 3), # input max element_count=3 (lambda x: 0 < len(x) >= 1, 1, 3), # input max element_count=3 (lambda x: 1 > len(x) <= 0, 0, 0), (lambda x: len(x) > 0 and len(x) > 0, 1, 3), # input max element_count=3 (lambda x: len(x) < 1 and len(x) < 1, 0, 0), (lambda x: len(x) > 1 and len(x) > 0, 2, 3), # input max element_count=3 (lambda x: len(x) < 1 and len(x) < 2, 0, 0), # Comparisons involving one literal and one variable (lambda x: 1 <= len(x) <= two, 1, 3), (lambda x: two <= len(x) <= 4, 0, 3), ], ids=get_pretty_function_description, ) @pytest.mark.parametrize( "strategy", [ st.lists(st.sampled_from([1, 2, 3]), unique=True), ], ids=get_pretty_function_description, ) @settings(max_examples=A_FEW) @given(data=st.data()) def test_filter_rewriting_lambda_len_unique_elements( data, strategy, predicate, start, end ): s = strategy.filter(predicate) unwrapped = unwrap_strategies(s) assert isinstance(unwrapped, FilteredStrategy) assert isinstance(unwrapped.filtered_strategy, type(unwrap_strategies(strategy))) for pred in unwrapped.flat_conditions: assert pred.__name__ == "" assert unwrapped.filtered_strategy.min_size == start assert unwrapped.filtered_strategy.max_size == end value = data.draw(s) assert predicate(value) @pytest.mark.parametrize( "predicate", [ (lambda x: len(x) < 3), (lambda x: len(x) > 5), ], ids=get_pretty_function_description, ) def test_does_not_rewrite_unsatisfiable_len_filter(predicate): strategy = st.lists(st.none(), min_size=4, max_size=4).filter(predicate) with pytest.raises(Unsatisfiable): check_can_generate_examples(strategy) # Rewriting to nothing() would correctly express the constraint. However # we don't want _only rewritable strategies_ to work in e.g. one_of, so: assert not strategy.is_empty @pytest.mark.parametrize( "method", ["match", "search", "findall", "fullmatch", "finditer", "split"] ) @pytest.mark.parametrize( "strategy, pattern", [ (st.text(), "ab+c"), (st.text(), "a|b"), (st.text(alphabet="abcdef"), "ab+c"), (st.text(min_size=5, max_size=10), "ab+c"), (st.binary(), b"ab+c"), (st.binary(), b"a|b"), (st.binary(min_size=5, max_size=10), b"ab+c"), ], ids=repr, ) @settings(max_examples=A_FEW) @given(data=st.data()) def test_regex_filter_rewriting(data, strategy, pattern, method): # This would raise a HealthCheck without rewriting, so checking that # we can draw a valid value is sufficient. predicate = getattr(re.compile(pattern), method) s = strategy.filter(predicate) if method in ("finditer", "split"): msg = r"You applied re.compile\(.+?\).\w+ as a filter, but this allows" with pytest.warns(HypothesisWarning, match=msg): value = data.draw(s) else: value = data.draw(s) assert predicate(value) @fails_with(TypeError) @given(st.text().filter(re.compile("abc").sub)) def test_error_on_method_which_requires_multiple_args(_): pass def test_dates_filter_rewriting(): today = dt.date.today() assert st.dates().filter(partial(operator.lt, dt.date.max)).is_empty assert st.dates().filter(partial(operator.gt, dt.date.min)).is_empty assert st.dates(min_value=today).filter(partial(operator.gt, today)).is_empty assert st.dates(max_value=today).filter(partial(operator.lt, today)).is_empty bare = unwrap_strategies(st.dates()) assert bare.filter(partial(operator.ge, dt.date.max)) is bare assert bare.filter(partial(operator.le, dt.date.min)) is bare new = bare.filter(partial(operator.le, today)) assert not new.is_empty assert new is not bare @pytest.mark.skipif( sys.version_info[:2] < (3, 14), reason="functools.Placeholder is new in 3.14" ) def test_partial_placeholder(): from functools import Placeholder assert st.integers(0, 5).filter(partial(operator.gt, Placeholder, 5)).is_empty s = unwrap_strategies( st.integers(-5, 5).filter(partial(operator.lt, Placeholder, 3)) ) assert (s.start, s.end) == (-5, 2) s = unwrap_strategies( st.integers(-5, 5).filter(partial(operator.le, Placeholder, 3)) ) assert (s.start, s.end) == (-5, 3) s = unwrap_strategies( st.integers(-5, 5).filter(partial(operator.gt, Placeholder, 3)) ) assert (s.start, s.end) == (4, 5) s = unwrap_strategies( st.integers(-5, 5).filter(partial(operator.ge, Placeholder, 3)) ) assert (s.start, s.end) == (3, 5) s = unwrap_strategies( st.integers(-5, 5).filter(partial(operator.eq, Placeholder, 3)) ) assert (s.start, s.end) == (3, 3) ================================================ FILE: hypothesis-python/tests/cover/test_filtered_strategy.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import hypothesis.strategies as st from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.strategies._internal.strategies import FilteredStrategy def test_filter_iterations_are_marked_as_discarded(): variable_equal_to_zero = 0 # non-local references disables filter-rewriting x = st.integers().filter(lambda x: x == variable_equal_to_zero) data = ConjectureData.for_choices([1, 0]) assert data.draw(x) == 0 assert data.has_discards def test_filtered_branches_are_all_filtered(): s = FilteredStrategy(st.integers() | st.text(), (bool,)) assert all(isinstance(x, FilteredStrategy) for x in s.branches) def test_filter_conditions_may_be_empty(): s = FilteredStrategy(st.integers(), conditions=()) s.condition(0) def test_nested_filteredstrategy_flattens_conditions(): s = FilteredStrategy( FilteredStrategy(st.text(), conditions=(bool,)), conditions=(len,), ) assert s.filtered_strategy is st.text() assert s.flat_conditions == (bool, len) ================================================ FILE: hypothesis-python/tests/cover/test_find.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from random import Random from hypothesis import Phase, find, settings, strategies as st from tests.common.utils import Why, xfail_on_crosshair @xfail_on_crosshair(Why.symbolic_outside_context) def test_find_uses_provided_random(): prev = None for _ in range(3): seen = None def test(v): if len(v) > 5: nonlocal seen if seen is not None: return v == seen else: seen = v return True result = find( st.text(), test, random=Random(13), settings=settings(phases=[Phase.generate], max_examples=1000), ) if prev is None: prev = result else: assert prev == result ================================================ FILE: hypothesis-python/tests/cover/test_flakiness.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys import pytest from hypothesis import HealthCheck, Verbosity, assume, example, given, reject, settings from hypothesis.core import StateForActualGivenExecution from hypothesis.errors import Flaky, FlakyFailure, Unsatisfiable, UnsatisfiedAssumption from hypothesis.internal.compat import ExceptionGroup from hypothesis.internal.conjecture.engine import MIN_TEST_CALLS from hypothesis.internal.scrutineer import Tracer from hypothesis.strategies import booleans, composite, integers, lists, random_module from tests.common.utils import Why, no_shrink, skipif_threading, xfail_on_crosshair class Nope(Exception): pass def test_fails_only_once_is_flaky(): first_call = True @given(integers()) def rude(x): nonlocal first_call if first_call: first_call = False raise Nope with pytest.raises(FlakyFailure, match="Falsified on the first call but") as e: rude() exceptions = e.value.exceptions assert len(exceptions) == 1 assert isinstance(exceptions[0], Nope) def test_fails_differently_is_flaky(): call_count = 0 class DifferentNope(Exception): pass @given(integers()) @settings(database=None) def rude(x): nonlocal call_count if x == 0: call_count += 1 if call_count > 1: raise Nope else: raise DifferentNope with pytest.raises(FlakyFailure, match="Inconsistent results from replaying") as e: rude() exceptions = e.value.exceptions assert len(exceptions) == 2 assert set(map(type, exceptions)) == {Nope, DifferentNope} @skipif_threading # executing into global scope @pytest.mark.skipif(sys.version_info < (3, 11), reason="except* syntax") def test_exceptiongroup_wrapped_naked_exception_is_flaky(): # Defer parsing until runtime, as "except*" is syntax error pre 3.11 rude_def = """ first_call = True def rude_fn(x): global first_call if first_call: first_call = False try: raise Nope except* Nope: raise """ exec(rude_def, globals()) rude = given(integers())(rude_fn) # noqa: F821 # defined by exec() with pytest.raises(FlakyFailure, match="Falsified on the first call but") as e: rude() exceptions = e.value.exceptions assert list(map(type, exceptions)) == [ExceptionGroup] assert list(map(type, exceptions[0].exceptions)) == [Nope] def test_gives_flaky_error_if_assumption_is_flaky(): seen = set() @given(integers()) @settings(verbosity=Verbosity.quiet, database=None) def oops(s): assume(s not in seen) seen.add(s) raise AssertionError with pytest.raises(FlakyFailure, match="Inconsistent results from replaying") as e: oops() exceptions = e.value.exceptions assert len(exceptions) == 2 assert isinstance(exceptions[0], AssertionError) assert isinstance(exceptions[1], UnsatisfiedAssumption) def test_flaky_with_context_when_fails_only_under_tracing(monkeypatch): # make anything fail under tracing monkeypatch.setattr(Tracer, "can_trace", staticmethod(lambda: True)) monkeypatch.setattr(Tracer, "__enter__", lambda *_: 1 / 0) # ensure tracing is always entered inside _execute_once_for_engine monkeypatch.setattr(StateForActualGivenExecution, "_should_trace", lambda _: True) @given(integers()) def test(x): pass with pytest.raises( FlakyFailure, match="failed on the first run but now succeeds" ) as e: test() exceptions = e.value.exceptions assert len(exceptions) == 1 assert isinstance(exceptions[0], ZeroDivisionError) @xfail_on_crosshair(Why.symbolic_outside_context) def test_does_not_attempt_to_shrink_flaky_errors(): values = [] @settings(database=None) @given(integers()) def test(x): values.append(x) assert len(values) != 1 with pytest.raises(FlakyFailure): test() # We try a total of ten calls in the generation phase, each usually a # unique value, looking briefly (and unsuccessfully) for another bug. assert 1 < len(set(values)) <= MIN_TEST_CALLS # We don't try any new values while shrinking, just execute the test # twice more (to check for flakiness and to raise the bug to the user). assert set(values) == set(values[:-2]) class SatisfyMe(Exception): pass @composite def single_bool_lists(draw): n = draw(integers(0, 20)) result = [False] * (n + 1) result[n] = True return result @xfail_on_crosshair(Why.nested_given) @example([True, False, False, False], [3], None) @example([False, True, False, False], [3], None) @example([False, False, True, False], [3], None) @example([False, False, False, True], [3], None) @settings( deadline=None, suppress_health_check=[HealthCheck.nested_given], max_examples=10 ) @given(lists(booleans()) | single_bool_lists(), lists(integers(1, 3)), random_module()) def test_failure_sequence_inducing(building, testing, rnd): buildit = iter(building) testit = iter(testing) def build(x): try: assume(not next(buildit)) except StopIteration: pass return x @given(integers().map(build)) @settings( verbosity=Verbosity.quiet, database=None, suppress_health_check=list(HealthCheck), phases=no_shrink, max_examples=10, ) def test(x): try: i = next(testit) except StopIteration: return if i == 1: return elif i == 2: reject() else: raise Nope try: test() except (Nope, Flaky, Unsatisfiable): pass except UnsatisfiedAssumption: raise SatisfyMe from None ================================================ FILE: hypothesis-python/tests/cover/test_float_nastiness.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys import warnings import pytest from hypothesis import assume, given, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.internal.floats import ( float_of, float_to_int, int_to_float, is_negative, next_down, next_up, ) from tests.common.debug import find_any, minimal try: import numpy except ImportError: numpy = None @pytest.mark.parametrize( ("lower", "upper"), [ # Exact values don't matter, but they're large enough so that x + y = inf. (9.9792015476736e291, 1.7976931348623157e308), (-sys.float_info.max, sys.float_info.max), ], ) @given(data=st.data()) def test_floats_are_in_range(data, lower, upper): t = data.draw(st.floats(lower, upper)) assert lower <= t <= upper @pytest.mark.parametrize("sign", [-1, 1]) def test_can_generate_both_zeros(sign): assert minimal(st.floats(), lambda x: math.copysign(1, x) == sign) == sign * 0.0 @pytest.mark.parametrize( ("l", "r"), [(-1.0, 1.0), (-0.0, 1.0), (-1.0, 0.0), (-sys.float_info.min, sys.float_info.min)], ) @pytest.mark.parametrize("sign", [-1, 1]) def test_can_generate_both_zeros_when_in_interval(l, r, sign): assert minimal(st.floats(l, r), lambda x: math.copysign(1, x) == sign) == sign * 0.0 @given(st.floats(0.0, 1.0)) def test_does_not_generate_negative_if_right_boundary_is_positive(x): assert math.copysign(1, x) == 1 @given(st.floats(-1.0, -0.0)) def test_does_not_generate_positive_if_right_boundary_is_negative(x): assert math.copysign(1, x) == -1 def test_half_bounded_generates_zero(): find_any(st.floats(min_value=-1.0), lambda x: x == 0.0) find_any(st.floats(max_value=1.0), lambda x: x == 0.0) @given(st.floats(max_value=-0.0)) def test_half_bounded_respects_sign_of_upper_bound(x): assert math.copysign(1, x) == -1 @given(st.floats(min_value=0.0)) def test_half_bounded_respects_sign_of_lower_bound(x): assert math.copysign(1, x) == 1 @given(st.floats(allow_nan=False)) def test_filter_nan(x): assert not math.isnan(x) @given(st.floats(allow_infinity=False)) def test_filter_infinity(x): assert not math.isinf(x) def test_can_guard_against_draws_of_nan(): """In this test we create a NaN value that naturally "tries" to shrink into the first strategy, where it is not permitted. This tests a case that is very unlikely to happen in random generation: When the unconstrained first branch of generating a float just happens to produce a NaN value. Here what happens is that we get a NaN from the *second* strategy, but this then shrinks into its unconstrained branch. The natural thing to happen is then to try to zero the branch parameter of the one_of, but that will put an illegal value there, so it's not allowed to happen. """ tagged_floats = st.one_of( st.tuples(st.just(0), st.floats(allow_nan=False)), st.tuples(st.just(1), st.floats(allow_nan=True)), ) tag, _f = minimal(tagged_floats, lambda x: math.isnan(x[1])) assert tag == 1 def test_very_narrow_interval(): upper_bound = -1.0 lower_bound = int_to_float(float_to_int(upper_bound) + 10) assert lower_bound < upper_bound @given(st.floats(lower_bound, upper_bound)) def test(f): assert lower_bound <= f <= upper_bound test() @given(st.floats()) def test_up_means_greater(x): hi = next_up(x) if not x < hi: assert ( (math.isnan(x) and math.isnan(hi)) or (x > 0 and math.isinf(x)) or (x == hi == 0 and is_negative(x) and not is_negative(hi)) ) @given(st.floats()) def test_down_means_lesser(x): lo = next_down(x) if not x > lo: assert ( (math.isnan(x) and math.isnan(lo)) or (x < 0 and math.isinf(x)) or (x == lo == 0 and is_negative(lo) and not is_negative(x)) ) @given(st.floats(allow_nan=False, allow_infinity=False)) def test_updown_roundtrip(val): assert val == next_up(next_down(val)) assert val == next_down(next_up(val)) @given(st.floats(width=32, allow_infinity=False)) def test_float32_can_exclude_infinity(x): assert not math.isinf(x) @given(st.floats(width=16, allow_infinity=False)) def test_float16_can_exclude_infinity(x): assert not math.isinf(x) @pytest.mark.parametrize( "kwargs", [ {"min_value": 10**5, "width": 16}, {"max_value": 10**5, "width": 16}, {"min_value": 10**40, "width": 32}, {"max_value": 10**40, "width": 32}, {"min_value": 10**400, "width": 64}, {"max_value": 10**400, "width": 64}, {"min_value": 10**400}, {"max_value": 10**400}, ], ) def test_out_of_range(kwargs): with pytest.raises(OverflowError): st.floats(**kwargs).validate() def test_disallowed_width(): with pytest.raises(InvalidArgument): st.floats(width=128).validate() def test_no_single_floats_in_range(): low = 2.0**25 + 1 high = low + 2 st.floats(low, high).validate() # Note: OK for 64bit floats with warnings.catch_warnings(): # Unrepresentable bounds are deprecated, but we're not testing that here warnings.simplefilter("ignore") with pytest.raises(InvalidArgument): st.floats(low, high, width=32).validate() # If the floats() strategy adds random floats to a value as large as 10^304 # without handling overflow, we are very likely to generate infinity. @given(st.floats(min_value=1e304, allow_infinity=False)) def test_finite_min_bound_does_not_overflow(x): assert not math.isinf(x) @given(st.floats(max_value=-1e304, allow_infinity=False)) def test_finite_max_bound_does_not_overflow(x): assert not math.isinf(x) @given(st.floats(0, 1, exclude_min=True, exclude_max=True)) def test_can_exclude_endpoints(x): assert 0 < x < 1 @given(st.floats(-math.inf, -1e307, exclude_min=True)) def test_can_exclude_neg_infinite_endpoint(x): assert not math.isinf(x) @given(st.floats(1e307, math.inf, exclude_max=True)) def test_can_exclude_pos_infinite_endpoint(x): assert not math.isinf(x) def test_exclude_infinite_endpoint_is_invalid(): with pytest.raises(InvalidArgument): st.floats(min_value=math.inf, exclude_min=True).validate() with pytest.raises(InvalidArgument): st.floats(max_value=-math.inf, exclude_max=True).validate() @pytest.mark.parametrize("lo,hi", [(True, False), (False, True), (True, True)]) @given(bound=st.floats(allow_nan=False, allow_infinity=False).filter(bool)) def test_exclude_entire_interval(lo, hi, bound): with pytest.raises(InvalidArgument, match=r"exclude_min=.+ and exclude_max="): st.floats(bound, bound, exclude_min=lo, exclude_max=hi).validate() def test_zero_intervals_are_OK(): st.floats(0.0, 0.0).validate() st.floats(-0.0, 0.0).validate() st.floats(-0.0, -0.0).validate() @pytest.mark.parametrize("lo", [0.0, -0.0]) @pytest.mark.parametrize("hi", [0.0, -0.0]) @pytest.mark.parametrize("exmin,exmax", [(True, False), (False, True), (True, True)]) def test_cannot_exclude_endpoint_with_zero_interval(lo, hi, exmin, exmax): with pytest.raises(InvalidArgument): st.floats(lo, hi, exclude_min=exmin, exclude_max=exmax).validate() WIDTHS = (64, 32, 16) @pytest.mark.parametrize("nonfloat", [st.nothing(), st.none()]) @given(data=st.data(), width=st.sampled_from(WIDTHS)) def test_fuzzing_floats_bounds(data, width, nonfloat): lo = data.draw(nonfloat | st.floats(allow_nan=False, width=width), label="lo") hi = data.draw(nonfloat | st.floats(allow_nan=False, width=width), label="hi") if lo is not None and hi is not None and lo > hi: lo, hi = hi, lo assume(lo != 0 or hi != 0) value = data.draw( st.floats(min_value=lo, max_value=hi, width=width, allow_nan=False), label="value", ) assert value == float_of(value, width=width) assert lo is None or lo <= value assert hi is None or value <= hi ================================================ FILE: hypothesis-python/tests/cover/test_float_utils.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from sys import float_info import pytest from hypothesis import example, given, strategies as st from hypothesis.internal.conjecture.choice import choice_equal, choice_permitted from hypothesis.internal.conjecture.provider_conformance import float_constraints from hypothesis.internal.floats import ( count_between_floats, make_float_clamper, next_down, next_up, sign_aware_lte, ) from tests.conjecture.common import float_constr def test_can_handle_straddling_zero(): assert count_between_floats(-0.0, 0.0) == 2 @pytest.mark.parametrize( "func,val", [ (next_up, math.nan), (next_up, math.inf), (next_up, -0.0), (next_down, math.nan), (next_down, -math.inf), (next_down, 0.0), ], ) def test_next_float_equal(func, val): if math.isnan(val): assert math.isnan(func(val)) else: assert func(val) == val # exponent comparisons: @example(float_constr(1, float_info.max), 0.0) @example(float_constr(1, float_info.max), 1.0) @example(float_constr(1, float_info.max), 10.0) @example(float_constr(1, float_info.max), float_info.max) @example(float_constr(1, float_info.max), math.inf) # mantissa comparisons: @example(float_constr(100.0001, 100.0003), 100.0001) @example(float_constr(100.0001, 100.0003), 100.0002) @example(float_constr(100.0001, 100.0003), 100.0003) @example(float_constr(100.0001, 100.0003, allow_nan=False), math.nan) @example(float_constr(0, 10, allow_nan=False), math.nan) @example(float_constr(0, 10, allow_nan=True), math.nan) # the branch coverage of resampling in the "out of range of smallest magnitude" case # relies on randomness from the mantissa. try a few different values. @example(float_constr(-4, -1, smallest_nonzero_magnitude=4), 4.0) @example(float_constr(-4, -1, smallest_nonzero_magnitude=4), 5.0) @example(float_constr(-4, -1, smallest_nonzero_magnitude=4), 6.0) @example(float_constr(1, 4, smallest_nonzero_magnitude=4), -4.0) @example(float_constr(1, 4, smallest_nonzero_magnitude=4), -5.0) @example(float_constr(1, 4, smallest_nonzero_magnitude=4), -6.0) @example(float_constr(-5e-324, -0.0), 3.0) @example(float_constr(0.0, 0.0), -0.0) @example(float_constr(-0.0, -0.0), 0.0) @given(float_constraints(), st.floats()) def test_float_clamper(constraints, input_value): min_value = constraints["min_value"] max_value = constraints["max_value"] allow_nan = constraints["allow_nan"] smallest_nonzero_magnitude = constraints["smallest_nonzero_magnitude"] clamper = make_float_clamper( min_value, max_value, smallest_nonzero_magnitude=smallest_nonzero_magnitude, allow_nan=allow_nan, ) clamped = clamper(input_value) if math.isnan(clamped): # we should only clamp to nan if nans are allowed. assert allow_nan else: # otherwise, we should have clamped to something in the permitted range. assert sign_aware_lte(min_value, clamped) assert sign_aware_lte(clamped, max_value) # if input_value was permitted in the first place, then the clamped value should # be the same as the input value. if choice_permitted( input_value, { "min_value": min_value, "max_value": max_value, "allow_nan": allow_nan, "smallest_nonzero_magnitude": smallest_nonzero_magnitude, }, ): assert choice_equal(input_value, clamped) ================================================ FILE: hypothesis-python/tests/cover/test_functions.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from inspect import signature import pytest from hypothesis import Verbosity, assume, find, given, settings, strategies as st from hypothesis.errors import InvalidArgument, InvalidState from hypothesis.reporting import with_reporter from hypothesis.strategies import booleans, functions, integers from tests.common.debug import check_can_generate_examples def func_a(): pass @given(functions(like=func_a, returns=booleans())) def test_functions_no_args(f): assert f.__name__ == "func_a" assert f is not func_a assert isinstance(f(), bool) def func_b(a, b, c): pass @given(functions(like=func_b, returns=booleans())) def test_functions_with_args(f): assert f.__name__ == "func_b" assert f is not func_b with pytest.raises(TypeError): f() assert isinstance(f(1, 2, 3), bool) def func_c(**kwargs): pass @given(functions(like=func_c, returns=booleans())) def test_functions_kw_args(f): assert f.__name__ == "func_c" assert f is not func_c with pytest.raises(TypeError): f(1, 2, 3) assert isinstance(f(a=1, b=2, c=3), bool) @given(functions(like=lambda: None, returns=booleans())) def test_functions_argless_lambda(f): assert f.__name__ == "" with pytest.raises(TypeError): f(1) assert isinstance(f(), bool) @given(functions(like=lambda a: None, returns=booleans())) def test_functions_lambda_with_arg(f): assert f.__name__ == "" with pytest.raises(TypeError): f() assert isinstance(f(1), bool) @pytest.mark.parametrize( "like,returns,pure", [ (None, booleans(), False), (lambda: None, "not a strategy", True), (lambda: None, booleans(), None), ], ) def test_invalid_arguments(like, returns, pure): with pytest.raises(InvalidArgument): check_can_generate_examples(functions(like=like, returns=returns, pure=pure)) def func_returns_str() -> str: return "a string" @given(functions(like=func_returns_str)) def test_functions_strategy_return_type_inference(f): result = f() assume(result != "a string") assert isinstance(result, str) def test_functions_valid_within_given_invalid_outside(): cache = None @given(functions()) def t(f): nonlocal cache cache = f assert f() is None t() with pytest.raises(InvalidState): cache() def test_can_call_default_like_arg(): # This test is somewhat silly, but coverage complains about the uncovered # branch for calling it otherwise and alternative workarounds are worse. like, returns, pure = signature(functions).parameters.values() assert like.default() is None assert returns.default is ... assert pure.default is False def func(arg, *, kwonly_arg): pass @given(functions(like=func)) def test_functions_strategy_with_kwonly_args(f): with pytest.raises(TypeError): f(1, 2) f(1, kwonly_arg=2) f(kwonly_arg=2, arg=1) def pure_func(arg1, arg2): pass @given( f=functions(like=pure_func, returns=integers(), pure=True), arg1=integers(), arg2=integers(), ) def test_functions_pure_with_same_args(f, arg1, arg2): # Same regardless of calling convention, unlike functools.lru_cache() expected = f(arg1, arg2) assert f(arg1, arg2) == expected assert f(arg1, arg2=arg2) == expected assert f(arg1=arg1, arg2=arg2) == expected assert f(arg2=arg2, arg1=arg1) == expected @given( f=functions(like=pure_func, returns=integers(), pure=True), arg1=integers(), arg2=integers(), ) def test_functions_pure_with_different_args(f, arg1, arg2): r1 = f(arg1, arg2) r2 = f(arg2, arg1) assume(r1 != r2) # If this is never true, the test will fail with Unsatisfiable @given( f1=functions(like=pure_func, returns=integers(), pure=True), f2=functions(like=pure_func, returns=integers(), pure=True), ) def test_functions_pure_two_functions_different_args_different_result(f1, f2): r1 = f1(1, 2) r2 = f2(3, 4) assume(r1 != r2) # If this is never true, the test will fail with Unsatisfiable @given( f1=functions(like=pure_func, returns=integers(), pure=True), f2=functions(like=pure_func, returns=integers(), pure=True), arg1=integers(), arg2=integers(), ) def test_functions_pure_two_functions_same_args_different_result(f1, f2, arg1, arg2): r1 = f1(arg1, arg2) r2 = f2(arg1, arg2) assume(r1 != r2) # If this is never true, the test will fail with Unsatisfiable @settings(verbosity=Verbosity.verbose) @given(functions(pure=False)) def test_functions_note_all_calls_to_impure_functions(f): ls = [] with with_reporter(ls.append): f() f() assert len(ls) == 2 @settings(verbosity=Verbosity.verbose) @given(functions(pure=True)) def test_functions_note_only_first_to_pure_functions(f): ls = [] with with_reporter(ls.append): f() f() assert len(ls) == 1 def test_functions_supports_find(): f = find( st.functions(like=pure_func, returns=st.integers(), pure=True), lambda x: True ) with pytest.raises(InvalidState): f(1, 2) assert f.__name__ == pure_func.__name__ ================================================ FILE: hypothesis-python/tests/cover/test_fuzz_one_input.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import io import unittest from operator import attrgetter from random import randbytes import pytest from hypothesis import Phase, given, settings, strategies as st from hypothesis.database import InMemoryExampleDatabase from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture.engine import shortlex @pytest.mark.parametrize( "buffer_type", [bytes, bytearray, memoryview, io.BytesIO], ids=attrgetter("__name__"), ) def test_fuzz_one_input(buffer_type): db = InMemoryExampleDatabase() seen = [] seeds = [] # This is a standard `@given` test, which we can also use as a fuzz target. # Note that we specify the DB so we can make more precise assertions, # and tighten the phases so we can be sure the failing examples come from fuzzing. @given(st.text()) @settings(database=db, phases=[Phase.reuse, Phase.shrink]) def test(s): seen.append(s) assert len(s) < 5, repr(s) # Before running fuzz_one_input, there's nothing in `db`, and so the test passes # (because example generation is disabled by the custom settings) with pytest.raises(unittest.SkipTest): # because this generates no examples test() assert len(seen) == 0 # If we run a lot of random bytestrings through fuzz_one_input, we'll eventually # find a failing example. with pytest.raises(AssertionError): for _ in range(1000): buf = randbytes(1000) seeds.append(buf) test.hypothesis.fuzz_one_input(buffer_type(buf)) # fuzz_one_input returns False for invalid bytestrings, due to e.g. assume(False) assert len(seen) <= len(seeds) # `db` contains exactly one failing example, which is either the most # recent seed that we tried or the pruned-and-canonicalised form of it. (saved_examples,) = db.data.values() assert len(saved_examples) == 1 assert shortlex(seeds[-1]) >= shortlex(next(iter(saved_examples))) # Now that we have a failure in `db`, re-running our test is sufficient to # reproduce it, *and shrink to a minimal example*. with pytest.raises(AssertionError): test() assert seen[-1] == "0" * 5 def test_can_fuzz_with_database_eq_None(): # This test exists to cover the can't-record-failure branch. @given(st.none()) @settings(database=None) def test(s): raise AssertionError with pytest.raises(AssertionError): test.hypothesis.fuzz_one_input(b"\x00\x00") def test_fuzzing_unsatisfiable_test_always_returns_None(): # There are no examples of `st.none().filter(bool)`, but while the Hypothesis # engine would give up, fuzz_one_input will just return None each time. @given(st.none().filter(bool)) @settings(database=None) def test(s): raise AssertionError("Unreachable because there are no valid examples") for _ in range(100): buf = randbytes(3) ret = test.hypothesis.fuzz_one_input(buf) assert ret is None def test_autopruning_of_returned_buffer(): @given(st.binary(min_size=4, max_size=4)) @settings(database=None) def test(s): pass # Unused portions of the input buffer are discarded from output. # (and canonicalised, but that's a no-op for fixed-length `binary()`) assert test.hypothesis.fuzz_one_input(b"deadbeef") == b"dead" def test_can_access_strategy_for_wrapped_test(): strategy = st.builds(object) @given(x=strategy) def addx(x, y): pass @given(strategy) def addy(x, y): pass assert addx.hypothesis._given_kwargs == {"x": strategy} assert addy.hypothesis._given_kwargs == {"y": strategy} @pytest.mark.parametrize( "buffers,db_size", [ ([b"aa", b"bb", b"cc", b"dd"], 1), # ascending -> only saves first ([b"dd", b"cc", b"bb", b"aa"], 4), # descending -> saves all ([b"cc", b"dd", b"aa", b"bb"], 2), # sawtooth -> saves cc then aa ([b"aa", b"bb", b"cc", b"XX"], 2), # two distinct errors -> saves both ], ) def test_fuzz_one_input_does_not_add_redundant_entries_to_database(buffers, db_size): db = InMemoryExampleDatabase() seen = [] @given(st.binary(min_size=2, max_size=2)) @settings(database=db) def test(s): seen.append(s) assert s != b"XX" raise AssertionError for buf in buffers: with pytest.raises(AssertionError): test.hypothesis.fuzz_one_input(buf) (saved_examples,) = db.data.values() assert seen == buffers assert len(saved_examples) == db_size def test_fuzzing_invalid_test_raises_error(): # Invalid: @given with too many positional arguments @given(st.integers(), st.integers()) def invalid_test(s): pass with pytest.raises(InvalidArgument, match="Too many positional arguments"): # access the property to check error happens during setup invalid_test.hypothesis.fuzz_one_input ================================================ FILE: hypothesis-python/tests/cover/test_given_error_conditions.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import assume, given, reject, settings from hypothesis._settings import all_settings from hypothesis.errors import InvalidArgument, Unsatisfiable from hypothesis.strategies import booleans, integers, nothing from tests.common.utils import fails_with @fails_with(Unsatisfiable) @given(booleans()) def test_raises_unsatisfiable_if_all_false_in_finite_set(x): reject() def test_does_not_raise_unsatisfiable_if_some_false_in_finite_set(): @given(booleans()) def test_assume_x(x): assume(x) test_assume_x() def test_raises_unsatisfiable_if_passed_explicit_nothing(): @given(x=nothing()) def test_never_runs(x): raise Exception("Can't ever execute this") with pytest.raises( Unsatisfiable, match=r"Cannot generate examples from empty strategy: x=nothing\(\)", ): test_never_runs() def test_error_if_has_no_hints(): @given(a=...) def inner(a): pass with pytest.raises(InvalidArgument): inner() def test_error_if_infer_all_and_has_no_hints(): @given(...) def inner(a): pass with pytest.raises(InvalidArgument): inner() def test_error_if_infer_is_posarg(): @given(..., ...) def inner(ex1: int, ex2: int): pass with pytest.raises(InvalidArgument): inner() def test_error_if_infer_is_posarg_mixed_with_kwarg(): @given(..., ex2=...) def inner(ex1: int, ex2: int): pass with pytest.raises(InvalidArgument): inner() def test_given_twice_is_an_error(): @settings(deadline=None) @given(booleans()) @given(integers()) def inner(a, b): pass with pytest.raises(InvalidArgument): inner() @fails_with(InvalidArgument) def test_given_is_not_a_class_decorator(): @given(integers()) class test_given_is_not_a_class_decorator: def __init__(self, i): pass def test_specific_error_for_coroutine_functions(): @settings(database=None) @given(booleans()) async def foo(x): pass with pytest.raises( InvalidArgument, match="Hypothesis doesn't know how to run async test functions", ): foo() @pytest.mark.parametrize("setting_name", all_settings) def test_suggests_at_settings_if_extra_kwarg_matches_setting_name(setting_name): val = 1 # dynamically create functions with an extra kwarg argument which happens to # match a settings variable. The user probably meant @settings. # exec is pretty cursed here, but it does work. namespace = {} exec( f""" @given(a=1, {setting_name}={val}) def foo(a): pass """, globals(), namespace, ) with pytest.raises( InvalidArgument, match=rf"Did you mean @settings\({setting_name}={val}\)\?", ): namespace["foo"]() ================================================ FILE: hypothesis-python/tests/cover/test_health_checks.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import time import pytest from hypothesis import HealthCheck, given, settings, strategies as st from hypothesis.control import assume, current_build_context from hypothesis.errors import FailedHealthCheck, InvalidArgument from hypothesis.internal.compat import int_from_bytes from hypothesis.stateful import ( RuleBasedStateMachine, initialize, invariant, rule, run_state_machine_as_test, ) from hypothesis.strategies import SearchStrategy from tests.common.utils import Why, no_shrink, skipif_time_unpatched, xfail_on_crosshair HEALTH_CHECK_SETTINGS = settings( max_examples=11, database=None, suppress_health_check=() ) def test_slow_generation_fails_a_health_check(): @settings(HEALTH_CHECK_SETTINGS, deadline=200) @given(st.integers().map(lambda x: time.sleep(0.2))) def test(x): pass with pytest.raises(FailedHealthCheck): test() def test_slow_generation_inline_fails_a_health_check(): @settings(HEALTH_CHECK_SETTINGS, deadline=200) @given(st.data()) def test(data): data.draw(st.integers().map(lambda x: time.sleep(0.2))) with pytest.raises(FailedHealthCheck): test() def test_default_health_check_can_weaken_specific(): import random @settings(HEALTH_CHECK_SETTINGS, suppress_health_check=list(HealthCheck)) @given(st.lists(st.integers(), min_size=1)) def test(x): random.choice(x) test() @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="nondeterministic" ) def test_suppressing_filtering_health_check(): forbidden = set() def unhealthy_filter(x): if len(forbidden) < 200: forbidden.add(x) return x not in forbidden @HEALTH_CHECK_SETTINGS @given(st.integers().filter(unhealthy_filter)) def test1(x): raise ValueError with pytest.raises(FailedHealthCheck): test1() forbidden = set() @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given(st.integers().filter(unhealthy_filter)) def test2(x): raise ValueError with pytest.raises(ValueError): test2() def test_filtering_everything_fails_a_health_check(): @given(st.integers().filter(lambda x: False)) @settings(database=None, suppress_health_check=()) def test(x): pass with pytest.raises(FailedHealthCheck, match="filter"): test() class fails_regularly(SearchStrategy): def do_draw(self, data): b = int_from_bytes(data.draw_bytes(2, 2)) assume(b == 3) def test_filtering_most_things_fails_a_health_check(): @given(fails_regularly()) @settings(database=None, phases=no_shrink, suppress_health_check=()) def test(x): if current_build_context().data.provider.avoid_realization: pytest.skip("symbolic backends can filter efficiently!") with pytest.raises(FailedHealthCheck, match="filter"): test() def test_returning_non_none_is_forbidden(): @given(st.integers()) def a(x): return 1 with pytest.raises(FailedHealthCheck): a() @skipif_time_unpatched # takes forever def test_the_slow_test_health_check_can_be_disabled(): @given(st.integers()) @settings(deadline=None) def a(x): time.sleep(1000) a() @skipif_time_unpatched # takes forever def test_the_slow_test_health_only_runs_if_health_checks_are_on(): @given(st.integers()) @settings(suppress_health_check=list(HealthCheck), deadline=None) def a(x): time.sleep(1000) a() class sample_test_runner: @given(st.none()) def test(self, _): pass def test_differing_executors_fails_health_check(): sample_test_runner().test() msg = re.escape(str(HealthCheck.differing_executors)) with pytest.raises(FailedHealthCheck, match=msg): sample_test_runner().test() def test_it_is_an_error_to_suppress_non_iterables(): with pytest.raises(InvalidArgument): settings(suppress_health_check=1) def test_it_is_an_error_to_suppress_non_healthchecks(): with pytest.raises(InvalidArgument): settings(suppress_health_check=["notahealthcheck"]) class ReturningRuleMachine(RuleBasedStateMachine): @rule() def r(self): return "any non-None value" class ReturningInitializeMachine(RuleBasedStateMachine): _ = rule()(lambda self: None) @initialize() def r(self): return "any non-None value" class ReturningInvariantMachine(RuleBasedStateMachine): _ = rule()(lambda self: None) @invariant(check_during_init=True) def r(self): return "any non-None value" @pytest.mark.parametrize( "cls", [ReturningRuleMachine, ReturningInitializeMachine, ReturningInvariantMachine] ) def test_stateful_returnvalue_healthcheck(cls): with pytest.raises(FailedHealthCheck): run_state_machine_as_test(cls, settings=settings()) def test_nested_given_raises_healthcheck(): @given(st.integers()) def f(n1): @given(st.integers()) def g(n2): pass g() with pytest.raises(FailedHealthCheck): f() def test_triply_nested_given_raises_healthcheck(): @given(st.integers()) @settings(max_examples=10) def f(n1): @given(st.integers()) @settings(max_examples=10) def g(n2): @given(st.integers()) @settings(max_examples=10) def h(n3): pass h() g() with pytest.raises(FailedHealthCheck): f() @xfail_on_crosshair(Why.nested_given) def test_can_suppress_nested_given(): @given(st.integers()) @settings(suppress_health_check=[HealthCheck.nested_given], max_examples=5) def f(n1): @given(st.integers()) @settings(max_examples=5) def g(n2): pass g() f() def test_cant_suppress_nested_given_on_inner(): # nested_given has to be suppressed at the function right above the nesting. # this isn't a principled design choice, but a limitation of how we access # the current settings. @given(st.integers()) @settings(max_examples=5) def f(n1): @given(st.integers()) @settings(suppress_health_check=[HealthCheck.nested_given], max_examples=5) def g(n2): pass g() with pytest.raises(FailedHealthCheck): f() @xfail_on_crosshair(Why.nested_given) def test_suppress_triply_nested_given(): # both suppressions are necessary here @given(st.integers()) @settings(suppress_health_check=[HealthCheck.nested_given], max_examples=5) def f(n1): @given(st.integers()) @settings(suppress_health_check=[HealthCheck.nested_given], max_examples=5) def g(n2): @given(st.integers()) @settings(max_examples=5) def h(n3): pass h() g() f() ================================================ FILE: hypothesis-python/tests/cover/test_interactive_example.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import subprocess import sys import warnings from decimal import Decimal import pytest from hypothesis import example, find, given, settings, strategies as st from hypothesis.errors import ( HypothesisException, InvalidArgument, NonInteractiveExampleWarning, Unsatisfiable, ) from hypothesis.internal.compat import WINDOWS from tests.common.debug import find_any from tests.common.utils import ( Why, fails_with, skipif_emscripten, skipif_threading, xfail_on_crosshair, ) pytest_plugins = "pytester" # .example() uses random.shuffle, which changes the global random state and # produces "do not use the `random` module inside strategies" deprecation warnings. # # Since we recommend against using .example() interactively, fixing this is # an enhancement, not a bug. pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason=".example() is not thread-safe", ) # Allow calling .example() without warnings for all tests in this module @pytest.fixture(scope="function", autouse=True) def _allow_noninteractive_example(): with warnings.catch_warnings(): warnings.simplefilter("ignore", NonInteractiveExampleWarning) yield def test_example_of_none_is_none(): assert st.none().example() is None def test_exception_in_compare_can_still_have_example(): st.one_of(st.none().map(lambda n: Decimal("snan")), st.just(Decimal(0))).example() @xfail_on_crosshair(Why.symbolic_outside_context) def test_does_not_always_give_the_same_example(): s = st.integers() assert len({s.example() for _ in range(100)}) >= 10 def test_raises_on_no_examples(): with pytest.raises(Unsatisfiable): st.nothing().example() @fails_with(HypothesisException) @example(False) @given(st.booleans()) def test_example_inside_given(b): st.integers().example() @fails_with(HypothesisException) def test_example_inside_find(): find(st.integers(0, 100), lambda x: st.integers().example()) @fails_with(HypothesisException) def test_example_inside_strategy(): find_any(st.booleans().map(lambda x: st.integers().example())) def test_raises_on_arbitrary_data(): with pytest.raises(InvalidArgument): st.data().example() def test_non_interactive_example_emits_warning(): # Revert the effect of the allow_noninteractive_example autouse fixture with warnings.catch_warnings(): warnings.simplefilter("always") with pytest.warns(NonInteractiveExampleWarning): st.text().example() EXAMPLE_GENERATING_TEST = """ from hypothesis import strategies as st def test_interactive_example(): st.integers().example() """ @skipif_threading # pytester not thread safe def test_selftests_exception_contains_note(pytester): # The note is added by a pytest hook, so we need to run it under pytest in a # subenvironment with (effectively) the same toplevel conftest. with warnings.catch_warnings(): warnings.simplefilter("error") pytester.makeconftest("from tests.conftest import *") result = pytester.runpytest_inprocess( pytester.makepyfile(EXAMPLE_GENERATING_TEST), "-p", "no:cacheprovider" ) assert "helper methods in tests.common.debug" in "\n".join(result.outlines) @skipif_emscripten def test_script_example_does_not_emit_warning(tmp_path): script = tmp_path / "script.py" script.write_text( "from hypothesis.strategies import integers\nintegers().example()\n", encoding="utf-8", ) # get rid of PYTEST_CURRENT_TEST, which we special-case as forcing this # warning to still appear env = os.environ.copy() env.pop("PYTEST_CURRENT_TEST") subprocess.check_call([sys.executable, "-Werror", str(script)], env=env) @skipif_emscripten @pytest.mark.skipif(WINDOWS, reason="pexpect.spawn not supported on Windows") def test_interactive_example_does_not_emit_warning(): import pexpect try: child = pexpect.spawn(f"{sys.executable} -Werror") child.expect(">>> ", timeout=10) except pexpect.exceptions.EOF: pytest.skip( "Unable to run python with -Werror. This may be because you are " "running from an old virtual environment - update your installed " "copy of `virtualenv` and then create a fresh environment." ) child.sendline("from hypothesis.strategies import none") child.sendline("none().example()") child.sendline("quit(code=0)") ================================================ FILE: hypothesis-python/tests/cover/test_internal_helpers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis.internal.floats import is_negative def test_is_negative_gives_good_type_error(): x = "foo" with pytest.raises(TypeError) as e: is_negative(x) assert repr(x) in e.value.args[0] ================================================ FILE: hypothesis-python/tests/cover/test_intervalset.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import HealthCheck, assume, example, given, settings, strategies as st from hypothesis.internal.conjecture.provider_conformance import ( interval_lists, intervals, ) from hypothesis.internal.intervalsets import IntervalSet # various tests in this file impose a max_codepoint restriction on intervals, # for performance. There may be possibilities for performance improvements in # IntervalSet itself as well. @given(intervals(max_codepoint=200)) def test_intervals_are_equivalent_to_their_lists(intervals): ls = list(intervals) assert len(ls) == len(intervals) for i in range(len(ls)): assert ls[i] == intervals[i] for i in range(1, len(ls) - 1): assert ls[-i] == intervals[-i] @given(intervals(max_codepoint=200)) def test_intervals_match_indexes(intervals): ls = list(intervals) for v in ls: assert ls.index(v) == intervals.index(v) @example(intervals=IntervalSet(((1, 1),)), v=0) @example(intervals=IntervalSet(()), v=0) @given(intervals(), st.integers(0, 0x10FFFF)) def test_error_for_index_of_not_present_value(intervals, v): assume(v not in intervals) with pytest.raises(ValueError): intervals.index(v) def test_validates_index(): with pytest.raises(IndexError): IntervalSet([])[1] with pytest.raises(IndexError): IntervalSet([[1, 10]])[11] with pytest.raises(IndexError): IntervalSet([[1, 10]])[-11] def test_index_above_is_index_if_present(): assert IntervalSet([[1, 10]]).index_above(1) == 0 assert IntervalSet([[1, 10]]).index_above(2) == 1 def test_index_above_is_length_if_higher(): assert IntervalSet([[1, 10]]).index_above(100) == 10 def intervals_to_set(ints): return set(IntervalSet(ints)) @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @example(x=[(0, 1), (3, 3)], y=[(1, 3)]) @example(x=[(0, 1)], y=[(0, 0), (1, 1)]) @example(x=[(0, 1)], y=[(1, 1)]) @given(interval_lists(max_codepoint=200), interval_lists(max_codepoint=200)) def test_subtraction_of_intervals(x, y): xs = intervals_to_set(x) ys = intervals_to_set(y) assume(not xs.isdisjoint(ys)) z = IntervalSet(x).difference(IntervalSet(y)).intervals assert z == tuple(sorted(z)) for a, b in z: assert a <= b assert intervals_to_set(z) == intervals_to_set(x) - intervals_to_set(y) @given(intervals(max_codepoint=200), intervals(max_codepoint=200)) def test_interval_intersection(x, y): assert set(x & y) == set(x) & set(y) assert set(x.intersection(y)) == set(x).intersection(y) def test_char_in_shrink_order(): xs = IntervalSet([(0, 256)]) assert xs[xs._idx_of_zero] == ord("0") assert xs[xs._idx_of_Z] == ord("Z") rewritten = [ord(xs.char_in_shrink_order(i)) for i in range(256)] assert rewritten != list(range(256)) assert sorted(rewritten) == sorted(range(256)) def test_index_from_char_in_shrink_order(): xs = IntervalSet([(0, 256)]) for i in xs: assert xs.index_from_char_in_shrink_order(xs.char_in_shrink_order(i)) == i def test_intervalset_equal(): xs1 = IntervalSet([(0, 256)]) xs2 = IntervalSet([(0, 256)]) assert xs1 == xs2 xs3 = IntervalSet([(0, 255)]) assert xs2 != xs3 ================================================ FILE: hypothesis-python/tests/cover/test_lambda_formatting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import runpy import sys import pytest from hypothesis.internal import lambda_sources from hypothesis.internal.conjecture.utils import identity from hypothesis.internal.reflection import get_pretty_function_description from tests.common.utils import skipif_threading @pytest.fixture(autouse=True, params=[True, False]) def clear_lambda_caches(request, monkeypatch): # Run all tests in this file twice, once using cache and once forcing # from-scratch generation if request.param: monkeypatch.setattr(lambda_sources, "LAMBDA_DESCRIPTION_CACHE", {}) monkeypatch.setattr(lambda_sources, "LAMBDA_DIGEST_DESCRIPTION_CACHE", {}) monkeypatch.setattr(lambda_sources, "AST_LAMBDAS_CACHE", {}) return request.param def test_bracket_whitespace_is_stripped(): # fmt: off t = lambda x: (x + 1 ) # fmt: on assert get_pretty_function_description(t) == "lambda x: x + 1" def test_can_have_unicode_in_lambda_sources(): t = lambda x: "é" not in x assert get_pretty_function_description(t) == "lambda x: 'é' not in x" @pytest.mark.skipif( sys.version_info < (3, 11), reason="nested lambdas fail compile-test" ) def test_can_get_descriptions_of_nested_lambdas_with_different_names(): # fmt: off ordered_pair = ( lambda right: [].map( lambda length: ())) # fmt: on assert ( get_pretty_function_description(ordered_pair) == "lambda right: [].map(lambda length: ())" ) def test_does_not_error_on_unparsable_source(): # fmt: off t = [ lambda x: \ # This will break ast.parse, but the brackets are needed for the real # parser to accept this lambda x][0] # fmt: on assert get_pretty_function_description(t) == "lambda x: x" @pytest.mark.skipif( sys.version_info < (3, 11), reason="nested lambdas fail compile-test" ) def test_separate_line_map_filter(): # this isn't intentionally testing nested lambdas, but hey, it's a nice bonus. # fmt: off f1 = ( lambda x: x .map(lambda y: y) ) f2 = ( lambda x: x .filter(lambda y: y) ) # fmt: on assert get_pretty_function_description(f1) == "lambda x: x.map(lambda y: y)" assert get_pretty_function_description(f2) == "lambda x: x.filter(lambda y: y)" def test_source_of_lambda_is_pretty(): assert get_pretty_function_description(lambda x: True) == "lambda x: True" def test_variable_names_are_not_pretty(): t = lambda x: True assert get_pretty_function_description(t) == "lambda x: True" def test_does_not_error_on_dynamically_defined_functions(): x = eval("lambda t: 1") assert get_pretty_function_description(x) == "lambda t: " def test_collapses_whitespace_nicely(): # fmt: off t = ( lambda x, y: 1 ) # fmt: on assert get_pretty_function_description(t) == "lambda x, y: 1" def test_is_not_confused_by_tuples(): p = (lambda x: x > 1, 2)[0] assert get_pretty_function_description(p) == "lambda x: x > 1" def test_strips_comments_from_the_end(): t = lambda x: 1 # A lambda comment assert get_pretty_function_description(t) == "lambda x: 1" def test_does_not_strip_hashes_within_a_string(): t = lambda x: "#" assert get_pretty_function_description(t) == "lambda x: '#'" def test_can_distinguish_between_two_lambdas_with_different_args(): a, b = (lambda x: 1, lambda y: 2) assert get_pretty_function_description(a) == "lambda x: 1" assert get_pretty_function_description(b) == "lambda y: 2" def test_can_distinguish_between_two_lambdas_with_different_constants(): a, b = (lambda x: 1, lambda x: 2) assert get_pretty_function_description(a) == "lambda x: 1" assert get_pretty_function_description(b) == "lambda x: 2" def test_can_distinguish_between_two_lambdas_with_different_signatures(): a, b = (lambda x: x, lambda y: y) assert get_pretty_function_description(a) == "lambda x: x" assert get_pretty_function_description(b) == "lambda y: y" def test_can_distinguish_between_two_lambdas_where_one_fails_localparse(): # fmt: off a, b = ( lambda x: x, lambda x: x or None, ) # fmt: on assert get_pretty_function_description(a) == "lambda x: x" assert get_pretty_function_description(b) == "lambda x: x or None" def test_does_not_get_confused_by_identical_lambdas(): a, b = (lambda x: 1, lambda x: 1) assert get_pretty_function_description(a) == "lambda x: 1" assert get_pretty_function_description(b) == "lambda x: 1" c = 1 lambda_capturing_globals = (lambda: c, lambda: 1) def test_lambda_capturing_globals(): assert get_pretty_function_description(lambda_capturing_globals[0]) == "lambda: c" assert get_pretty_function_description(lambda_capturing_globals[1]) == "lambda: 1" def test_lambda_capturing_locals(): const = 1 a, b = (lambda: const, lambda: 1) assert get_pretty_function_description(a) == "lambda: const" assert get_pretty_function_description(b) == "lambda: 1" def test_can_distinguish_between_two_lambdas_with_different_captures(): # fmt: off a = 1; f1 = lambda x=a: x; a=2; f2=lambda x=a: x # noqa: E702 # fmt: on assert get_pretty_function_description(f1) == "lambda x=1: x" assert get_pretty_function_description(f2) == "lambda x=2: x" def test_lambda_source_break_after_bracket(): # Issue #4498 regression test, inspect.getsource only sees the first line # fmt: off f = ( lambda x: x or None ) # fmt: on source = get_pretty_function_description(f) assert source == "lambda x: x or None" def test_lambda_source_break_after_def_with_brackets(): # fmt: off f = (lambda n: 'aaa') # fmt: on source = get_pretty_function_description(f) assert source == "lambda n: 'aaa'" def test_lambda_source_break_after_def_with_line_continuation(): # fmt: off f = lambda n:\ 'aaa' # fmt: on source = get_pretty_function_description(f) assert source == "lambda n: 'aaa'" def arg_decorator(*s): def accept(f): return s return accept @arg_decorator(lambda x: x + 1) def plus_one(): pass @arg_decorator(lambda x: x + 1, lambda y: y * 2) def two_decorators(): pass def test_can_extract_lambda_repr_in_a_decorator(): assert get_pretty_function_description(plus_one[0]) == "lambda x: x + 1" def test_can_extract_two_lambdas_from_a_decorator_if_args_differ(): a, b = two_decorators assert get_pretty_function_description(a) == "lambda x: x + 1" assert get_pretty_function_description(b) == "lambda y: y * 2" @arg_decorator(lambda: ()) def to_brackets(): pass def test_can_handle_brackets_in_decorator_argument(): assert get_pretty_function_description(to_brackets[0]) == "lambda: ()" @arg_decorator(identity(lambda x: x + 1)) def decorator_with_wrapper(): pass def test_can_handle_nested_lambda_in_decorator_argument(): assert ( get_pretty_function_description(decorator_with_wrapper[0]) == "lambda x: x + 1" ) @skipif_threading # concurrent writes to the same file def test_modifying_lambda_source_code_returns_unknown(tmp_path): # see https://github.com/HypothesisWorks/hypothesis/pull/4452 test_module = tmp_path / "test_module.py" test_module.write_text( "# line one\n\ntest_lambda = lambda x: x * 2", encoding="utf-8" ) module_globals = runpy.run_path(str(test_module)) test_module.write_text("# line one\n\n# line two", encoding="utf-8") assert ( get_pretty_function_description(module_globals["test_lambda"]) == "lambda x: " ) @skipif_threading # concurrent writes to the same file def test_adding_other_lambda_does_not_confuse(tmp_path): test_module = tmp_path / "test_module.py" test_module.write_text( "# line one\ntest_lambda = lambda x: x * 2", encoding="utf-8" ) module_globals = runpy.run_path(str(test_module)) test_module.write_text( "# line one\nlambda x: x\n\n\ntest_lambda = lambda x: x * 2", encoding="utf-8" ) f1 = module_globals["test_lambda"] assert get_pretty_function_description(f1) == "lambda x: x * 2" @skipif_threading # concurrent writes to the same file def test_changing_lambda_confuses(tmp_path, allow_unknown_lambdas, clear_lambda_caches): if not clear_lambda_caches: pytest.skip("requires clean cache") test_module = tmp_path / "test_module.py" test_module.write_text("test_lambda = lambda x: x * 2", encoding="utf-8") module_globals = runpy.run_path(str(test_module)) test_module.write_text("lambda x: x", encoding="utf-8") f1 = module_globals["test_lambda"] assert get_pretty_function_description(f1) == "lambda x: " @skipif_threading # concurrent writes to the same file @pytest.mark.skipif(sys.version_info[:2] < (3, 11), reason="not checked before 3.11") def test_that_test_harness_raises_on_unknown_lambda(tmp_path): test_module = tmp_path / "test_module.py" test_module.write_text("test_lambda = lambda x: x * 2", encoding="utf-8") module_globals = runpy.run_path(str(test_module)) test_module.write_text("lambda x: x", encoding="utf-8") f1 = module_globals["test_lambda"] with pytest.raises(AssertionError): assert get_pretty_function_description(f1) == "lambda x: " @skipif_threading # concurrent writes to the same file def test_source_with_syntax_error(tmp_path, allow_unknown_lambdas, clear_lambda_caches): if not clear_lambda_caches: pytest.skip("requires clean cache") test_module = tmp_path / "test_module.py" test_module.write_text("test_lambda = lambda x: x * 2", encoding="utf-8") module_globals = runpy.run_path(str(test_module)) f1 = module_globals["test_lambda"] test_module.write_text("line 1", encoding="utf-8") assert get_pretty_function_description(f1) == "lambda x: " @skipif_threading # concurrent writes to the same file def test_unknown_is_not_stuck(tmp_path, allow_unknown_lambdas, clear_lambda_caches): if not clear_lambda_caches: pytest.skip("requires clean cache") test_module = tmp_path / "test_module.py" test_module.write_text("test_lambda = lambda x: x * 2", encoding="utf-8") module_globals = runpy.run_path(str(test_module)) f1 = module_globals["test_lambda"] test_module.write_text("line 1", encoding="utf-8") assert get_pretty_function_description(f1) == "lambda x: " test_module2 = tmp_path / "test_module2.py" test_module2.write_text("test_lambda = lambda x: x * 2", encoding="utf-8") module_globals2 = runpy.run_path(str(test_module2)) f2 = module_globals2["test_lambda"] # f2 matches f1 in the digest cache, ensure that it gets a proper description assert get_pretty_function_description(f1) == "lambda x: " assert get_pretty_function_description(f2) == "lambda x: x * 2" ================================================ FILE: hypothesis-python/tests/cover/test_lazy_import.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import subprocess import sys from tests.common.utils import skipif_emscripten SHOULD_NOT_IMPORT_TEST_RUNNERS = """ import sys import unittest from hypothesis import given, strategies as st class TestDoesNotImportRunners(unittest.TestCase): strat = st.integers() | st.floats() | st.sampled_from(["a", "b"]) @given(strat) def test_does_not_import_unittest2(self, x): assert "unittest2" not in sys.modules @given(strat) def test_does_not_import_nose(self, x): assert "nose" not in sys.modules assert "nose2" not in sys.modules @given(strat) def test_does_not_import_pytest(self, x): assert "pytest" not in sys.modules if __name__ == '__main__': unittest.main() """ @skipif_emscripten def test_hypothesis_does_not_import_test_runners(tmp_path): # We obviously can't use pytest to check that pytest is not imported, # so for consistency we use unittest for all three non-stdlib test runners. # It's unclear which of our dependencies is importing unittest, but # since I doubt it's causing any spurious failures I don't really care. # See https://github.com/HypothesisWorks/hypothesis/pull/2204 fname = tmp_path / "test.py" fname.write_text(SHOULD_NOT_IMPORT_TEST_RUNNERS, encoding="utf-8") subprocess.check_call( [sys.executable, str(fname)], env={**os.environ, "HYPOTHESIS_NO_PLUGINS": "1"}, ) ================================================ FILE: hypothesis-python/tests/cover/test_lookup.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import abc import builtins import collections import contextlib import datetime import enum import inspect import io import random import re import string import sys import typing import warnings from dataclasses import dataclass from inspect import signature from numbers import Real from typing import ( Dict as _Dict, FrozenSet as _FrozenSet, List as _List, Set as _Set, Tuple as _Tuple, Type as _Type, ) import pytest from hypothesis import HealthCheck, assume, given, settings, strategies as st from hypothesis.errors import InvalidArgument, ResolutionFailed, SmallSearchSpaceWarning from hypothesis.internal.compat import get_type_hints from hypothesis.internal.conjecture.junkdrawer import stack_depth_of_caller from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies import from_type from hypothesis.strategies._internal import types from tests.common.debug import ( assert_all_examples, assert_no_examples, assert_simple_property, check_can_generate_examples, find_any, minimal, ) from tests.common.utils import fails_with, temp_registered # we'll continue testing the typing variants until their removal from the stdlib # ruff: noqa: UP006, UP035, UP045, UP007 sentinel = object() BUILTIN_TYPES = tuple( v for v in vars(builtins).values() if isinstance(v, type) and v.__name__ != "BuiltinImporter" ) generics = sorted( ( t for t in types._global_type_lookup # We ignore TypeVar, because it is not a Generic type: if isinstance(t, types.typing_root_type) and t != typing.TypeVar and ( sys.version_info[:2] <= (3, 11) or t != getattr(typing, "ByteString", object()) ) ), key=str, ) @pytest.mark.parametrize("typ", generics, ids=repr) @settings( suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], database=None, ) @given(data=st.data()) def test_resolve_typing_module(data, typ): ex = data.draw(from_type(typ)) if typ in (typing.BinaryIO, typing.TextIO): assert isinstance(ex, io.IOBase) elif isinstance(typ, typing._ProtocolMeta): pass elif (typing.get_origin(typ) or typ) is type and not isinstance(type, type): assert ex is type or isinstance(ex, typing.TypeVar) else: assert isinstance(ex, typ) @pytest.mark.parametrize("typ", [typing.Any, typing.Union]) def test_does_not_resolve_special_cases(typ): with pytest.raises(InvalidArgument): check_can_generate_examples(from_type(typ)) @pytest.mark.parametrize( "typ,instance_of", [ (typing.Union[int, str], (int, str)), (int | str, (int, str)), (typing.Optional[int], (int, type(None))), (int | None, (int, type(None))), ], ) @given(data=st.data()) def test_specialised_scalar_types(data, typ, instance_of): ex = data.draw(from_type(typ)) assert isinstance(ex, instance_of) def test_typing_Type_int(): for t in (type[int], type["int"], _Type[int], _Type["int"]): assert_simple_property(from_type(t), lambda x: x is int) @given(from_type(type[str | list]) | from_type(_Type[str | list])) def test_typing_Type_Union(ex): assert ex in (str, list) @pytest.mark.parametrize( "typ", [ pytest.param( getattr(collections.abc, "ByteString", ...), marks=pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="removed"), ), typing.Match, typing.Pattern, re.Match, re.Pattern, ], ids=repr, ) @given(data=st.data()) def test_rare_types(data, typ): ex = data.draw(from_type(typ)) with warnings.catch_warnings(): if sys.version_info[:2] >= (3, 12): warnings.simplefilter("ignore", DeprecationWarning) # ByteString is deprecated in Python 3.12 assert isinstance(ex, typ) class Elem: pass @pytest.mark.parametrize( "typ,coll_type", [ (_Set[Elem], set), (_FrozenSet[Elem], frozenset), (_Dict[Elem, None], dict), (set[Elem], set), (frozenset[Elem], frozenset), # (dict[Elem, None], dict), # FIXME this should work (typing.DefaultDict[Elem, None], collections.defaultdict), (typing.KeysView[Elem], type({}.keys())), (typing.ValuesView[Elem], type({}.values())), (_List[Elem], list), (_Tuple[Elem], tuple), (_Tuple[Elem, ...], tuple), (list[Elem], list), (tuple[Elem], tuple), (tuple[Elem, ...], tuple), (typing.Iterator[Elem], typing.Iterator), (typing.Sequence[Elem], typing.Sequence), (typing.Iterable[Elem], typing.Iterable), (typing.Mapping[Elem, None], typing.Mapping), (typing.Container[Elem], typing.Container), (typing.NamedTuple("A_NamedTuple", (("elem", Elem),)), tuple), (typing.Counter[Elem], typing.Counter), (typing.Deque[Elem], typing.Deque), ], ids=repr, ) @given(data=st.data()) def test_specialised_collection_types(data, typ, coll_type): ex = data.draw(from_type(typ)) assert isinstance(ex, coll_type) instances = [isinstance(elem, Elem) for elem in ex] assert all(instances) assume(instances) # non-empty collections without calling len(iterator) class ElemValue: pass @pytest.mark.parametrize( "typ,coll_type", [ (typing.ChainMap[Elem, ElemValue], typing.ChainMap), (typing.DefaultDict[Elem, ElemValue], typing.DefaultDict), (typing.OrderedDict[Elem, ElemValue], typing.OrderedDict), ], ids=repr, ) @given(data=st.data()) def test_specialised_mapping_types(data, typ, coll_type): ex = data.draw(from_type(typ).filter(len)) assert isinstance(ex, coll_type) instances = [isinstance(elem, Elem) for elem in ex] assert all(instances) assert all(isinstance(elem, ElemValue) for elem in ex.values()) @given(from_type(typing.ItemsView[Elem, Elem]).filter(len)) def test_ItemsView(ex): # See https://github.com/python/typing/issues/177 assert isinstance(ex, type({}.items())) assert all(isinstance(elem, tuple) and len(elem) == 2 for elem in ex) assert all(all(isinstance(e, Elem) for e in elem) for elem in ex) @pytest.mark.parametrize("generic", [typing.Match, typing.Pattern]) @pytest.mark.parametrize("typ", [bytes, str]) @given(data=st.data()) def test_regex_types(data, generic, typ): x = data.draw(from_type(generic[typ])) assert isinstance(x[0] if generic is typing.Match else x.pattern, typ) @given(x=...) def test_Generator(x: typing.Generator[Elem, None, ElemValue]): assert isinstance(x, typing.Generator) try: while True: e = next(x) assert isinstance(e, Elem) x.send(None) # The generators we create don't check the send type except StopIteration as stop: assert isinstance(stop.value, ElemValue) def test_Optional_minimises_to_None(): assert minimal(from_type(typing.Optional[int]), lambda ex: True) is None @pytest.mark.parametrize("n", [0, 1, 5]) @pytest.mark.parametrize("t", [tuple, _Tuple]) def test_variable_length_tuples(t, n): type_ = t[int, ...] check_can_generate_examples(from_type(type_).filter(lambda ex: len(ex) == n)) def test_lookup_overrides_defaults(): sentinel = object() with temp_registered(int, st.just(sentinel)): @given(from_type(list[int])) def inner_1(ex): assert all(elem is sentinel for elem in ex) inner_1() @given(from_type(list[int])) def inner_2(ex): assert all(isinstance(elem, int) for elem in ex) inner_2() def test_register_generic_typing_strats(): # I don't expect anyone to do this, but good to check it works as expected with temp_registered( typing.Sequence, types._global_type_lookup[set], ): # We register sets for the abstract sequence type, which masks subtypes # from supertype resolution but not direct resolution assert_all_examples( from_type(typing.Sequence[int]), lambda ex: isinstance(ex, set) ) assert_all_examples( from_type(typing.Container[int]), lambda ex: not isinstance(ex, typing.Sequence), ) assert_all_examples(from_type(list[int]), lambda ex: isinstance(ex, list)) def if_available(name): try: return getattr(typing, name) except AttributeError: return pytest.param(name, marks=[pytest.mark.skip]) @pytest.mark.parametrize( "typ", [ typing.Sequence, typing.Container, typing.Mapping, typing.Reversible, typing.SupportsBytes, typing.SupportsAbs, typing.SupportsComplex, typing.SupportsFloat, typing.SupportsInt, typing.SupportsRound, if_available("SupportsIndex"), ], ids=get_pretty_function_description, ) def test_resolves_weird_types(typ): check_can_generate_examples(from_type(typ)) class Foo: def __init__(self, x): pass class Bar(Foo): pass class Baz(Foo): pass st.register_type_strategy(Bar, st.builds(Bar, st.integers())) st.register_type_strategy(Baz, st.builds(Baz, st.integers())) @pytest.mark.parametrize( "var,expected", [ (typing.TypeVar("V"), object), (typing.TypeVar("V", bound=int), int), (typing.TypeVar("V", bound=Foo), (Bar, Baz)), (typing.TypeVar("V", bound=int | str), (int, str)), (typing.TypeVar("V", int, str), (int, str)), ], ) @settings(suppress_health_check=[HealthCheck.too_slow]) @given(data=st.data()) def test_typevar_type_is_consistent(data, var, expected): strat = st.from_type(var) v1 = data.draw(strat) v2 = data.draw(strat) assume(v1 != v2) # Values may vary, just not types assert type(v1) == type(v2) assert isinstance(v1, expected) def test_distinct_typevars_same_constraint(): A = typing.TypeVar("A", int, str) B = typing.TypeVar("B", int, str) find_any( st.tuples(st.from_type(A), st.from_type(B)), lambda ab: type(ab[0]) != type(ab[1]), ) def test_distinct_typevars_distinct_type(): """Ensures that two different type vars have at least one different type in their strategies.""" A = typing.TypeVar("A") B = typing.TypeVar("B") find_any( st.tuples(st.from_type(A), st.from_type(B)), lambda ab: type(ab[0]) != type(ab[1]), ) A = typing.TypeVar("A") def same_type_args(a: A, b: A): assert type(a) == type(b) @given(st.builds(same_type_args)) def test_same_typevars_same_type(_): """Ensures that single type argument will always have the same type in a single context.""" def test_typevars_can_be_redefined(): """We test that one can register a custom strategy for all type vars.""" A = typing.TypeVar("A") with temp_registered(typing.TypeVar, st.just(1)): assert_all_examples(st.from_type(A), lambda obj: obj == 1) def test_typevars_can_be_redefine_with_factory(): """We test that one can register a custom strategy for all type vars.""" A = typing.TypeVar("A") with temp_registered(typing.TypeVar, lambda thing: st.just(thing.__name__)): assert_all_examples(st.from_type(A), lambda obj: obj == "A") def test_typevars_can_be_resolved_conditionally(): sentinel = object() A = typing.TypeVar("A") B = typing.TypeVar("B") def resolve_type_var(thing): assert thing in (A, B) if thing == A: return st.just(sentinel) return NotImplemented with temp_registered(typing.TypeVar, resolve_type_var): assert_simple_property(st.from_type(A), lambda x: x is sentinel) # We've re-defined the default TypeVar resolver, so there is no fallback. # This causes the lookup to fail. with pytest.raises(InvalidArgument): check_can_generate_examples(st.from_type(B)) def annotated_func(a: int, b: int = 2, *, c: int, d: int = 4): return a + b + c + d def test_issue_946_regression(): # Turned type hints into kwargs even if the required posarg was passed check_can_generate_examples(st.builds(annotated_func, st.integers())) @pytest.mark.parametrize( "thing", [ annotated_func, # Works via typing.get_type_hints typing.NamedTuple("N", [("a", int)]), # Falls back to inspection int, # Fails; returns empty dict ], ) def test_can_get_type_hints(thing): assert isinstance(get_type_hints(thing), dict) def test_force_builds_to_infer_strategies_for_default_args(): # By default, leaves args with defaults and minimises to 2+4=6 assert minimal(st.builds(annotated_func), lambda ex: True) == 6 # Inferring integers() for args makes it minimise to zero assert minimal(st.builds(annotated_func, b=..., d=...), lambda ex: True) == 0 def non_annotated_func(a, b=2, *, c, d=4): pass def test_cannot_pass_infer_as_posarg(): with pytest.raises(InvalidArgument): check_can_generate_examples(st.builds(annotated_func, ...)) def test_cannot_force_inference_for_unannotated_arg(): with pytest.raises(InvalidArgument): check_can_generate_examples(st.builds(non_annotated_func, a=..., c=st.none())) with pytest.raises(InvalidArgument): check_can_generate_examples(st.builds(non_annotated_func, a=st.none(), c=...)) class UnknownType: def __init__(self, arg): pass class UnknownAnnotatedType: def __init__(self, arg: int): pass @given(st.from_type(UnknownAnnotatedType)) def test_builds_for_unknown_annotated_type(ex): assert isinstance(ex, UnknownAnnotatedType) def unknown_annotated_func(a: UnknownType, b=2, *, c: UnknownType, d=4): pass def test_raises_for_arg_with_unresolvable_annotation(): with pytest.raises(ResolutionFailed): check_can_generate_examples(st.builds(unknown_annotated_func)) with pytest.raises(ResolutionFailed): check_can_generate_examples( st.builds(unknown_annotated_func, a=st.none(), c=...) ) @given(a=..., b=...) def test_can_use_type_hints(a: int, b: float): assert isinstance(a, int) assert isinstance(b, float) def test_error_if_has_unresolvable_hints(): @given(a=...) def inner(a: UnknownType): pass with pytest.raises(InvalidArgument): inner() def test_resolves_NewType(): typ = typing.NewType("T", int) nested = typing.NewType("NestedT", typ) uni = typing.NewType("UnionT", int | None) assert_simple_property(from_type(typ), lambda x: isinstance(x, int)) assert_simple_property(from_type(nested), lambda x: isinstance(x, int)) assert_simple_property(from_type(uni), lambda x: isinstance(x, (int, type(None)))) find_any(from_type(uni), lambda x: isinstance(x, int)) find_any(from_type(uni), lambda x: isinstance(x, type(None))) @pytest.mark.parametrize("is_handled", [True, False]) def test_resolves_NewType_conditionally(is_handled): sentinel = object() typ = typing.NewType("T", int) def resolve_custom_strategy(thing): assert thing is typ if is_handled: return st.just(sentinel) return NotImplemented with temp_registered(typ, resolve_custom_strategy): if is_handled: assert_simple_property(st.from_type(typ), lambda x: x is sentinel) else: assert_simple_property(st.from_type(typ), lambda x: isinstance(x, int)) E = enum.Enum("E", "a b c") @given(from_type(E)) def test_resolves_enum(ex): assert isinstance(ex, E) @pytest.mark.parametrize("resolver", [from_type, st.sampled_from]) def test_resolves_flag_enum(resolver): # Storing all combinations takes O(2^n) memory. Using an enum of 52 # members in this test ensures that we won't try! F = enum.Flag("F", " ".join(string.ascii_letters)) # Checks for combination coverage are found in nocover/test_sampled_from @given(resolver(F)) def inner(ex): assert isinstance(ex, F) inner() class AnnotatedTarget: def __init__(self, a: int, b: int): pass def method(self, a: int, b: int): pass @pytest.mark.parametrize("target", [AnnotatedTarget, AnnotatedTarget(1, 2).method]) @pytest.mark.parametrize( "args,kwargs", [ ((), {}), ((1,), {}), ((1, 2), {}), ((), {"a": 1}), ((), {"b": 2}), ((), {"a": 1, "b": 2}), ], ) def test_required_args(target, args, kwargs): # Mostly checking that `self` (and only self) is correctly excluded check_can_generate_examples( st.builds( target, *map(st.just, args), **{k: st.just(v) for k, v in kwargs.items()} ) ) class AnnotatedNamedTuple(typing.NamedTuple): a: str @given(st.builds(AnnotatedNamedTuple)) def test_infers_args_for_namedtuple_builds(thing): assert isinstance(thing.a, str) @given(st.from_type(AnnotatedNamedTuple)) def test_infers_args_for_namedtuple_from_type(thing): assert isinstance(thing.a, str) @given(st.builds(AnnotatedNamedTuple, a=st.none())) def test_override_args_for_namedtuple(thing): assert thing.a is None @pytest.mark.parametrize("thing", [typing.Optional, list, type, _List, _Type]) def test_cannot_resolve_bare_forward_reference(thing): t = thing["ConcreteFoo"] with pytest.raises(InvalidArgument): check_can_generate_examples(st.from_type(t)) class Tree: def __init__(self, left: typing.Optional["Tree"], right: typing.Optional["Tree"]): self.left = left self.right = right def __repr__(self): return f"Tree({self.left}, {self.right})" @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~19 mins; datastructure explosion https://github.com/pschanely/hypothesis-crosshair/issues/27", ) @given(tree=st.builds(Tree)) def test_resolving_recursive_type(tree): assert isinstance(tree, Tree) class TypedTree(typing.TypedDict): nxt: typing.Optional["TypedTree"] def test_resolving_recursive_typeddict(): assert_simple_property( st.from_type(TypedTree), lambda tree: isinstance(tree, dict) and len(tree) == 1 and "nxt" in tree, ) class MyList: def __init__(self, nxt: typing.Optional["MyList"] = None): self.nxt = nxt def __repr__(self): return f"MyList({self.nxt})" def __eq__(self, other): return type(self) == type(other) and self.nxt == other.nxt @given(lst=st.from_type(MyList)) def test_resolving_recursive_type_with_defaults(lst): assert isinstance(lst, MyList) def test_recursive_type_with_defaults_minimizes_to_defaults(): assert minimal(from_type(MyList), lambda ex: True) == MyList() class MutualA: def __init__(self, nxt: typing.Optional["MutualB"]): self.nxt = nxt def __repr__(self): return f"A({self.nxt})" class MutualB: def __init__(self, nxt: typing.Optional["MutualA"]): self.nxt = nxt def __repr__(self): return f"B({self.nxt})" @given(nxt=st.from_type(MutualA)) def test_resolving_mutually_recursive_types(nxt): i = 0 while nxt: assert isinstance(nxt, [MutualA, MutualB][i % 2]) nxt = nxt.nxt i += 1 def test_resolving_mutually_recursive_types_with_limited_stack(): orig_recursionlimit = sys.getrecursionlimit() current_stack_depth = stack_depth_of_caller() sys.setrecursionlimit(current_stack_depth + 100) try: @given(nxt=st.from_type(MutualA)) def test(nxt): pass test() finally: assert sys.getrecursionlimit() == current_stack_depth + 100 sys.setrecursionlimit(orig_recursionlimit) class A_with_default: def __init__(self, nxt: typing.Optional["B_with_default"] = None): self.nxt = nxt def __repr__(self): return f"A_with_default({self.nxt})" class B_with_default: def __init__(self, nxt: typing.Optional["A_with_default"] = None): self.nxt = nxt def __repr__(self): return f"B_with_default({self.nxt})" @given(nxt=st.from_type(A_with_default)) def test_resolving_mutually_recursive_types_with_defaults(nxt): # This test is required to cover the raise/except part of the recursion # check in from_type, see # https://github.com/HypothesisWorks/hypothesis/issues/3655. If the # skip-nondefaulted-args check is removed, this test becomes redundant. i = 0 while nxt: assert isinstance(nxt, [A_with_default, B_with_default][i % 2]) nxt = nxt.nxt i += 1 class SomeClass: def __init__(self, value: int, next_node: typing.Optional["SomeClass"]) -> None: assert value > 0 self.value = value self.next_node = next_node def __repr__(self) -> str: return f"SomeClass({self.value}, next_node={self.next_node})" def test_resolving_recursive_type_with_registered_constraint(): with temp_registered( SomeClass, st.builds(SomeClass, value=st.integers(min_value=1)) ): @given(s=st.from_type(SomeClass)) def test(s): assert isinstance(s, SomeClass) test() def test_resolving_recursive_type_with_registered_constraint_not_none(): with temp_registered( SomeClass, st.builds(SomeClass, value=st.integers(min_value=1)) ): s = st.from_type(SomeClass) print(s, s.wrapped_strategy) find_any(s, lambda s: s.next_node is not None) @given(from_type(tuple[()]) | from_type(_Tuple[()])) def test_resolves_empty_Tuple_issue_1583_regression(ex): # See e.g. https://github.com/python/mypy/commit/71332d58 assert ex == () def test_can_register_NewType(): Name = typing.NewType("Name", str) st.register_type_strategy(Name, st.just("Eric Idle")) assert_simple_property(st.from_type(Name), lambda x: x == "Eric Idle") @given(st.from_type(typing.Callable)) def test_resolves_bare_callable_to_function(f): val = f() assert val is None with pytest.raises(TypeError): f(1) @given(st.from_type(typing.Callable[[str], int])) def test_resolves_callable_with_arg_to_function(f): val = f("1") assert isinstance(val, int) @given(st.from_type(typing.Callable[..., int])) def test_resolves_ellipses_callable_to_function(f): val = f() assert isinstance(val, int) f(1) f(1, 2, 3) f(accepts_kwargs_too=1) class AbstractFoo(abc.ABC): @abc.abstractmethod def foo(self): pass class ConcreteFoo(AbstractFoo): def foo(self): pass @given(st.from_type(AbstractFoo)) def test_can_resolve_abstract_class(instance): assert isinstance(instance, ConcreteFoo) instance.foo() class AbstractBar(abc.ABC): @abc.abstractmethod def bar(self): pass @fails_with(ResolutionFailed) @given(st.from_type(AbstractBar)) def test_cannot_resolve_abstract_class_with_no_concrete_subclass(instance): raise AssertionError("test body unreachable as strategy cannot resolve") @fails_with(ResolutionFailed) @given(st.from_type(type["ConcreteFoo"])) def test_cannot_resolve_type_with_forwardref(instance): raise AssertionError("test body unreachable as strategy cannot resolve") @fails_with(ResolutionFailed) @given(st.from_type(_Type["ConcreteFoo"])) def test_cannot_resolve_type_with_forwardref_old(instance): raise AssertionError("test body unreachable as strategy cannot resolve") @pytest.mark.parametrize("typ", [typing.Hashable, typing.Sized]) @given(data=st.data()) def test_inference_on_generic_collections_abc_aliases(typ, data): # regression test for inference bug on types that are just aliases # types for simple interfaces in collections abc and take no args # the typing module such as Hashable and Sized # see https://github.com/HypothesisWorks/hypothesis/issues/2272 value = data.draw(st.from_type(typ)) assert isinstance(value, typ) @given(st.from_type(typing.Sequence[set])) def test_bytestring_not_treated_as_generic_sequence(val): # Check that we don't fall into the specific problem from # https://github.com/HypothesisWorks/hypothesis/issues/2257 assert not isinstance(val, bytes) # Check it hasn't happened again from some other non-generic sequence type. for x in val: assert isinstance(x, set) @pytest.mark.parametrize( "type_", [int, Real, object, typing.Union[int, str], typing.Union[Real, str]] ) def test_bytestring_is_valid_sequence_of_int_and_parent_classes(type_): find_any( st.from_type(typing.Sequence[type_]), lambda val: isinstance(val, bytes), ) @pytest.mark.parametrize("protocol", [typing.SupportsAbs, typing.SupportsRound]) @given(data=st.data()) def test_supportsop_types_support_protocol(protocol, data): # test values drawn from SupportsOp types are indeed considered instances # of that type. value = data.draw(st.from_type(protocol)) # check that we aren't somehow generating instances of the protocol itself assert value.__class__ != protocol assert issubclass(type(value), protocol) @pytest.mark.parametrize("restrict_custom_strategy", [True, False]) def test_generic_aliases_can_be_conditionally_resolved_by_registered_function( restrict_custom_strategy, ): # Check that a custom strategy function may provide no strategy for a # generic alias request like Container[T]. We test this under two scenarios: # - where CustomContainer CANNOT be generated from requests for Container[T] # (only for requests for exactly CustomContainer[T]) # - where CustomContainer CAN be generated from requests for Container[T] T = typing.TypeVar("T") @dataclass class CustomContainer(typing.Container[T]): content: T def __contains__(self, value: object) -> bool: return self.content == value def get_custom_container_strategy(thing): if restrict_custom_strategy and typing.get_origin(thing) != CustomContainer: return NotImplemented return st.builds( CustomContainer, content=st.from_type(typing.get_args(thing)[0]) ) with temp_registered(CustomContainer, get_custom_container_strategy): def is_custom_container_with_str(example): return isinstance(example, CustomContainer) and isinstance( example.content, str ) def is_non_custom_container(example): return isinstance(example, typing.Container) and not isinstance( example, CustomContainer ) assert_all_examples( st.from_type(CustomContainer[str]), is_custom_container_with_str ) # If the strategy function is restricting, it doesn't return a strategy # for requests for Container[...], so it's never generated. When not # restricting, it is generated. if restrict_custom_strategy: assert_all_examples( st.from_type(typing.Container[str]), is_non_custom_container ) else: find_any(st.from_type(typing.Container[str]), is_custom_container_with_str) find_any(st.from_type(typing.Container[str]), is_non_custom_container) @pytest.mark.parametrize( "protocol, typ", [ (typing.SupportsFloat, float), (typing.SupportsInt, int), (typing.SupportsBytes, bytes), (typing.SupportsComplex, complex), ], ) @given(data=st.data()) def test_supportscast_types_support_protocol_or_are_castable(protocol, typ, data): value = data.draw(st.from_type(protocol)) # check that we aren't somehow generating instances of the protocol itself assert value.__class__ != protocol # test values drawn from the protocol types either support the protocol # or can be cast to typ assert issubclass(type(value), protocol) or types.can_cast(typ, value) def test_can_cast(): assert types.can_cast(int, "0") assert not types.can_cast(int, "abc") @pytest.mark.parametrize("type_", [datetime.timezone, datetime.tzinfo]) def test_timezone_lookup(type_): assert issubclass(type_, datetime.tzinfo) assert_all_examples(st.from_type(type_), lambda t: isinstance(t, type_)) @pytest.mark.parametrize( "typ", [ _Set[typing.Hashable], _FrozenSet[typing.Hashable], _Dict[typing.Hashable, int], set[typing.Hashable], frozenset[typing.Hashable], dict[typing.Hashable, int], ], ) @settings(suppress_health_check=[HealthCheck.data_too_large]) @given(data=st.data()) def test_generic_collections_only_use_hashable_elements(typ, data): data.draw(from_type(typ)) @given(st.sets(st.integers() | st.binary(), min_size=2)) def test_no_byteswarning(_): pass @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="Crosshair is too much slower at hashing values", ) def test_hashable_type_unhashable_value(): # Decimal("snan") is not hashable; we should be able to generate it. # See https://github.com/HypothesisWorks/hypothesis/issues/2320 find_any( from_type(typing.Hashable), lambda x: not types._can_hash(x), settings(max_examples=10**5), ) def test_unhashable_type(): class UnhashableMeta(type): __hash__ = None class UnhashableType(metaclass=UnhashableMeta): pass assert_simple_property( st.from_type(UnhashableType), lambda x: isinstance(x, UnhashableType) ) class _EmptyClass: def __init__(self, value=-1) -> None: pass @pytest.mark.parametrize( "typ,repr_", [ (int, "integers()"), (list[str], "lists(text())"), (_List[str], "lists(text())"), ("not a type", "from_type('not a type')"), (random.Random, "randoms()"), (_EmptyClass, "from_type(tests.cover.test_lookup._EmptyClass)"), ( st.SearchStrategy[str], "from_type(hypothesis.strategies.SearchStrategy[str])", ), ], ) def test_repr_passthrough(typ, repr_): assert repr(st.from_type(typ)) == repr_ class TreeForwardRefs(typing.NamedTuple): val: int l: typing.Optional["TreeForwardRefs"] r: typing.Optional["TreeForwardRefs"] @given(st.builds(TreeForwardRefs)) def test_resolves_forward_references_outside_annotations(t): assert isinstance(t, TreeForwardRefs) def constructor(a: str = None): # noqa # deprecated implicit optional, for testing pass class WithOptionalInSignature: __signature__ = inspect.signature(constructor) __annotations__ = typing.get_type_hints(constructor) def __init__(self, **kwargs): assert set(kwargs) == {"a"} self.a = kwargs["a"] def test_compat_get_type_hints_aware_of_None_default(): # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/2648 strategy = st.builds(WithOptionalInSignature, a=...) find_any(strategy, lambda x: x.a is None) find_any(strategy, lambda x: x.a is not None) if sys.version_info[:2] >= (3, 11): # https://docs.python.org/3.11/library/typing.html#typing.get_type_hints assert typing.get_type_hints(constructor)["a"] == str else: assert typing.get_type_hints(constructor)["a"] == typing.Optional[str] assert inspect.signature(constructor).parameters["a"].annotation == str _ValueType = typing.TypeVar("_ValueType") class Wrapper(typing.Generic[_ValueType]): _inner_value: _ValueType def __init__(self, inner_value: _ValueType) -> None: self._inner_value = inner_value @given(st.builds(Wrapper)) def test_issue_2603_regression(built): """It was impossible to build annotated classes with constructors.""" assert isinstance(built, Wrapper) class AnnotatedConstructor(typing.Generic[_ValueType]): value: _ValueType # the same name we have in `__init__` def __init__(self, value: int) -> None: """By this example we show, that ``int`` is more important than ``_ValueType``.""" assert isinstance(value, int) @given(st.data()) def test_constructor_is_more_important(data): """Constructor types should take precedence over all other annotations.""" data.draw(st.builds(AnnotatedConstructor)) def use_signature(self, value: str) -> None: ... class AnnotatedConstructorWithSignature(typing.Generic[_ValueType]): value: _ValueType # the same name we have in `__init__` __signature__ = signature(use_signature) def __init__(self, value: int) -> None: """By this example we show, that ``__signature__`` is the most important source.""" assert isinstance(value, str) def selfless_signature(value: str) -> None: ... class AnnotatedConstructorWithSelflessSignature(AnnotatedConstructorWithSignature): __signature__ = signature(selfless_signature) def really_takes_str(value: int) -> None: """By this example we show, that ``__signature__`` is the most important source.""" assert isinstance(value, str) really_takes_str.__signature__ = signature(selfless_signature) @pytest.mark.parametrize( "thing", [ AnnotatedConstructorWithSignature, AnnotatedConstructorWithSelflessSignature, really_takes_str, ], ) def test_signature_is_the_most_important_source(thing): """Signature types should take precedence over all other annotations.""" check_can_generate_examples(st.builds(thing)) class AnnotatedAndDefault: def __init__(self, foo: bool | None = None): self.foo = foo def test_from_type_can_be_default_or_annotation(): find_any(st.from_type(AnnotatedAndDefault), lambda x: x.foo is None) find_any(st.from_type(AnnotatedAndDefault), lambda x: isinstance(x.foo, bool)) @pytest.mark.parametrize("t", BUILTIN_TYPES, ids=lambda t: t.__name__) def test_resolves_builtin_types(t): with warnings.catch_warnings(): warnings.simplefilter("ignore", SmallSearchSpaceWarning) assert_simple_property(st.from_type(t), lambda v: isinstance(v, t)) @pytest.mark.parametrize("t", BUILTIN_TYPES, ids=lambda t: t.__name__) @given(data=st.data()) @settings(max_examples=20) def test_resolves_forwardrefs_to_builtin_types(t, data): if t.__name__ == "object" and settings.get_current_profile_name() == "threading": # from_type(ForwardRef("object")) pulls from register_type_strategy, # and depending on threading I've seen `st.builds(Bar, st.integers())` # (from this file) be registered in one iteration and not the next, # causing Hypothesis to raise FlakyStrategyDefinition. # # (I would also expect st.from_type(object) to have this problem, but # I haven't seen that error under threading, yet). pytest.skip("ForwardRef('object') is inherently flaky under concurrency") s = st.from_type(typing.ForwardRef(t.__name__)) v = data.draw(s) assert isinstance(v, t) @pytest.mark.parametrize("t", BUILTIN_TYPES, ids=lambda t: t.__name__) def test_resolves_type_of_builtin_types(t): assert_simple_property(st.from_type(type[t.__name__]), lambda v: v is t) @given( st.from_type(type[typing.Union["str", "int"]]) | st.from_type(_Type[typing.Union["str", "int"]]) ) def test_resolves_type_of_union_of_forwardrefs_to_builtins(x): assert x in (str, int) @pytest.mark.parametrize( "type_", [ # Old-style `List` because `list[int]() == list()`, so no need for the hint. getattr(typing, "List", None)[int], pytest.param( typing.Optional[int], marks=pytest.mark.skipif( sys.version_info >= (3, 14), reason="different error on 3.14+" ), ), ], ) def test_builds_suggests_from_type(type_): with pytest.raises( InvalidArgument, match=re.escape(f"try using from_type({type_!r})") ): check_can_generate_examples(st.builds(type_)) try: check_can_generate_examples(st.builds(type_, st.just("has an argument"))) raise AssertionError("Expected strategy to raise an error") except TypeError as err: assert not isinstance(err, InvalidArgument) @pytest.mark.skipif(sys.version_info < (3, 14), reason="different error on 3.14+") @pytest.mark.parametrize("type_", [typing.Optional[int]]) def test_builds_suggests_from_type_on_construction(type_): with pytest.raises( InvalidArgument, match=re.escape(f"Try using from_type({type_!r})") ): check_can_generate_examples(st.builds(type_)) with pytest.raises( InvalidArgument, match=re.escape(f"Try using from_type({type_!r})") ): check_can_generate_examples(st.builds(type_, st.just("has an argument"))) def test_builds_mentions_no_type_check(): @typing.no_type_check def f(x: int): pass msg = "@no_type_check decorator prevented Hypothesis from inferring a strategy" with pytest.raises(TypeError, match=msg): check_can_generate_examples(st.builds(f)) class TupleSubtype(tuple): pass def test_tuple_subclasses_not_generic_sequences(): # see https://github.com/HypothesisWorks/hypothesis/issues/3767. with temp_registered(TupleSubtype, st.builds(TupleSubtype)): s = st.from_type(typing.Sequence[int]) assert_no_examples(s, lambda x: isinstance(x, tuple)) def test_custom_strategy_function_resolves_types_conditionally(): sentinel = object() class A: pass class B(A): pass class C(A): pass def resolve_custom_strategy_for_b(thing): if thing == B: return st.just(sentinel) return NotImplemented with contextlib.ExitStack() as stack: stack.enter_context(temp_registered(B, resolve_custom_strategy_for_b)) stack.enter_context(temp_registered(C, st.builds(C))) # C's strategy can be used for A, but B's cannot because its function # only returns a strategy for requests for exactly B. assert_all_examples(st.from_type(A), lambda example: type(example) == C) assert_all_examples(st.from_type(B), lambda example: example is sentinel) assert_all_examples(st.from_type(C), lambda example: type(example) == C) class CustomInteger(int): def __init__(self, value: int, /) -> None: if not isinstance(value, int): raise TypeError @given(...) def test_from_type_resolves_required_posonly_args(n: CustomInteger): # st.builds() does not infer for positional arguments, but st.from_type() # does. See e.g. https://stackoverflow.com/q/79199376/ for motivation. assert isinstance(n, CustomInteger) class MyProtocol(typing.Protocol): pass def test_issue_4194_regression(): # this was an edge case where we were calling issubclass on something # that was not a type, which errored. I don't have a more principled test # case or name for this. inner = typing.Union[typing.Sequence["A"], MyProtocol] A = typing.Union[typing.Sequence[inner], MyProtocol] with ( temp_registered(MyProtocol, st.just(b"")), temp_registered(typing.ForwardRef("A"), st.integers()), ): find_any(st.from_type(A)) ================================================ FILE: hypothesis-python/tests/cover/test_lookup_py310.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import strategies as st from tests.common.debug import find_any def test_native_unions(): s = st.from_type(int | list[str]) find_any(s, lambda x: isinstance(x, int)) find_any(s, lambda x: isinstance(x, list)) ================================================ FILE: hypothesis-python/tests/cover/test_lookup_py314.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Buffer from dataclasses import dataclass import pytest from hypothesis import given, strategies as st from tests.common.debug import find_any @dataclass class A: constant = 42 x: int # see https://docs.python.org/3/reference/datamodel.html#python-buffer-protocol # and https://peps.python.org/pep-0688/ def __buffer__(self, flags): return memoryview( self.constant.to_bytes() + self.x.to_bytes(length=32, signed=True) ) @given(st.from_type(memoryview[A])) def test_resolve_bufferlike_memoryview(v): assert isinstance(v, memoryview) assert v[0] == A.constant assert len(v) == 1 + 32 def test_errors_when___buffer___not_implemented(): class NoBuffer: pass @given(st.from_type(memoryview[NoBuffer])) def f(v): pass with pytest.raises( TypeError, match="a bytes-like object is required, not 'NoBuffer'" ): f() def test_resolve_Buffer(): s = st.from_type(Buffer) # on 3.12, we can generate neither. On 3.13, we can generate bytearray, because # the removal of ByteString stops blocking bytearray from being the maximal # registered type. On 3.14, we can generate both, because memoryview becomes # generic. find_any(s, lambda v: isinstance(v, memoryview)) find_any(s, lambda v: isinstance(v, bytearray)) ================================================ FILE: hypothesis-python/tests/cover/test_lookup_py37.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import collections import collections.abc import contextlib import re import pytest from hypothesis import assume, given class Elem: pass class Value: pass def check(t, ex): assert isinstance(ex, t) assert all(isinstance(e, Elem) for e in ex) assume(ex) @given(...) def test_resolving_standard_tuple1_as_generic(x: tuple[Elem]): check(tuple, x) @given(...) def test_resolving_standard_tuple2_as_generic(x: tuple[Elem, Elem]): check(tuple, x) @given(...) def test_resolving_standard_tuple_variadic_as_generic(x: tuple[Elem, ...]): check(tuple, x) @given(...) def test_resolving_standard_list_as_generic(x: list[Elem]): check(list, x) @given(...) def test_resolving_standard_dict_as_generic(x: dict[Elem, Value]): check(dict, x) assert all(isinstance(e, Value) for e in x.values()) @given(...) def test_resolving_standard_set_as_generic(x: set[Elem]): check(set, x) @given(...) def test_resolving_standard_frozenset_as_generic(x: frozenset[Elem]): check(frozenset, x) @given(...) def test_resolving_standard_deque_as_generic(x: collections.deque[Elem]): check(collections.deque, x) @given(...) def test_resolving_standard_defaultdict_as_generic( x: collections.defaultdict[Elem, Value], ): check(collections.defaultdict, x) assert all(isinstance(e, Value) for e in x.values()) @given(...) def test_resolving_standard_ordered_dict_as_generic( x: collections.OrderedDict[Elem, Value], ): check(collections.OrderedDict, x) assert all(isinstance(e, Value) for e in x.values()) @given(...) def test_resolving_standard_counter_as_generic(x: collections.Counter[Elem]): check(collections.Counter, x) assume(any(x.values())) # Check that we generated at least one nonzero count @given(...) def test_resolving_standard_chainmap_as_generic(x: collections.ChainMap[Elem, Value]): check(collections.ChainMap, x) assert all(isinstance(e, Value) for e in x.values()) @given(...) def test_resolving_standard_iterable_as_generic(x: collections.abc.Iterable[Elem]): check(collections.abc.Iterable, x) @given(...) def test_resolving_standard_iterator_as_generic(x: collections.abc.Iterator[Elem]): check(collections.abc.Iterator, x) @given(...) def test_resolving_standard_generator_as_generic( x: collections.abc.Generator[Elem, None, Value], ): assert isinstance(x, collections.abc.Generator) try: while True: e = next(x) assert isinstance(e, Elem) x.send(None) # The generators we create don't check the send type except StopIteration as stop: assert isinstance(stop.value, Value) @given(...) def test_resolving_standard_reversible_as_generic(x: collections.abc.Reversible[Elem]): check(collections.abc.Reversible, x) @given(...) def test_resolving_standard_container_as_generic(x: collections.abc.Container[Elem]): check(collections.abc.Container, x) @given(...) def test_resolving_standard_collection_as_generic(x: collections.abc.Collection[Elem]): check(collections.abc.Collection, x) @given(...) def test_resolving_standard_callable_ellipsis(x: collections.abc.Callable[..., Elem]): assert isinstance(x, collections.abc.Callable) assert callable(x) # ... implies *args, **kwargs; as would any argument types assert isinstance(x(), Elem) assert isinstance(x(1, 2, 3, a=4, b=5, c=6), Elem) @given(...) def test_resolving_standard_callable_no_args(x: collections.abc.Callable[[], Elem]): assert isinstance(x, collections.abc.Callable) assert callable(x) # [] implies that no arguments are accepted assert isinstance(x(), Elem) with pytest.raises(TypeError): x(1) with pytest.raises(TypeError): x(a=1) @given(...) def test_resolving_standard_collections_set_as_generic(x: collections.abc.Set[Elem]): check(collections.abc.Set, x) @given(...) def test_resolving_standard_collections_mutableset_as_generic( x: collections.abc.MutableSet[Elem], ): check(collections.abc.MutableSet, x) @given(...) def test_resolving_standard_mapping_as_generic(x: collections.abc.Mapping[Elem, Value]): check(collections.abc.Mapping, x) assert all(isinstance(e, Value) for e in x.values()) @given(...) def test_resolving_standard_mutable_mapping_as_generic( x: collections.abc.MutableMapping[Elem, Value], ): check(collections.abc.MutableMapping, x) assert all(isinstance(e, Value) for e in x.values()) @given(...) def test_resolving_standard_sequence_as_generic(x: collections.abc.Sequence[Elem]): check(collections.abc.Sequence, x) @given(...) def test_resolving_standard_mutable_sequence_as_generic( x: collections.abc.MutableSequence[Elem], ): check(collections.abc.MutableSequence, x) @given(...) def test_resolving_standard_keysview_as_generic(x: collections.abc.KeysView[Elem]): check(collections.abc.KeysView, x) @given(...) def test_resolving_standard_itemsview_as_generic( x: collections.abc.ItemsView[Elem, Value], ): assert isinstance(x, collections.abc.ItemsView) assert all(isinstance(e, Elem) and isinstance(v, Value) for e, v in x) assume(x) @given(...) def test_resolving_standard_valuesview_as_generic(x: collections.abc.ValuesView[Elem]): check(collections.abc.ValuesView, x) # Weird interaction with fixes in PR #2952 # # 2025-08-11: this gets even weirder with 3.14, where memoryview is in fact a # context manager, and so gets resolved for AbstractContextManager[Elem] here. # (But then errors during generation, because Elem does not implement __buffer__). @pytest.mark.xfail @given(...) def test_resolving_standard_contextmanager_as_generic( x: contextlib.AbstractContextManager[Elem], ): assert isinstance(x, contextlib.AbstractContextManager) @given(...) def test_resolving_standard_re_match_bytes_as_generic(x: re.Match[bytes]): assert isinstance(x, re.Match) assert isinstance(x[0], bytes) @given(...) def test_resolving_standard_re_match_str_as_generic(x: re.Match[str]): assert isinstance(x, re.Match) assert isinstance(x[0], str) @given(...) def test_resolving_standard_re_pattern_bytes_as_generic(x: re.Pattern[bytes]): assert isinstance(x, re.Pattern) assert isinstance(x.pattern, bytes) @given(...) def test_resolving_standard_re_pattern_str_as_generic(x: re.Pattern[str]): assert isinstance(x, re.Pattern) assert isinstance(x.pattern, str) ================================================ FILE: hypothesis-python/tests/cover/test_lookup_py38.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import dataclasses import re import typing from types import SimpleNamespace import pytest from hypothesis import example, given, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.internal.reflection import ( convert_positional_arguments, get_pretty_function_description, ) from hypothesis.strategies import from_type from tests.common.debug import ( assert_simple_property, check_can_generate_examples, find_any, ) from tests.common.utils import fails_with, temp_registered @given(st.data()) def test_typing_Final(data): value = data.draw(from_type(typing.Final[int])) assert isinstance(value, int) @pytest.mark.parametrize("value", ["dog", b"goldfish", 42, 63.4, -80.5, False]) def test_typing_Literal(value): assert_simple_property(from_type(typing.Literal[value]), lambda v: v == value) @given(st.data()) def test_typing_Literal_nested(data): lit = typing.Literal values = [ (lit["hamster", 0], ("hamster", 0)), (lit[26, False, "bunny", 130], (26, False, "bunny", 130)), (lit[lit[1]], {1}), (lit[lit[1], 2], {1, 2}), (lit[1, lit[2], 3], {1, 2, 3}), (lit[lit[lit[1], lit[2]], lit[lit[3], lit[4]]], {1, 2, 3, 4}), ] literal_type, flattened_literals = data.draw(st.sampled_from(values)) assert data.draw(st.from_type(literal_type)) in flattened_literals class A(typing.TypedDict): a: int @given(from_type(A)) def test_simple_typeddict(value): assert type(value) == dict assert set(value) == {"a"} assert isinstance(value["a"], int) class B(A, total=False): # a is required, b is optional b: bool @given(from_type(B)) def test_typeddict_with_optional(value): assert type(value) == dict assert set(value).issubset({"a", "b"}) assert isinstance(value["a"], int) if "b" in value: assert isinstance(value["b"], bool) def test_simple_optional_key_is_optional(): # Optional keys are not currently supported, as PEP-589 leaves no traces # at runtime. See https://github.com/python/cpython/pull/17214 find_any(from_type(B), lambda d: "b" not in d) class C(B): # a is required, b is optional, c is required again c: str @given(from_type(C)) def test_typeddict_with_optional_then_required_again(value): assert type(value) == dict assert set(value).issubset({"a", "b", "c"}) assert isinstance(value["a"], int) if "b" in value: assert isinstance(value["b"], bool) assert isinstance(value["c"], str) class NestedDict(typing.TypedDict): inner: A @given(from_type(NestedDict)) def test_typeddict_with_nested_value(value): assert type(value) == dict assert set(value) == {"inner"} assert isinstance(value["inner"]["a"], int) def test_layered_optional_key_is_optional(): # Optional keys are not currently supported, as PEP-589 leaves no traces # at runtime. See https://github.com/python/cpython/pull/17214 find_any(from_type(C), lambda d: "b" not in d) @dataclasses.dataclass(slots=False, frozen=False) class Node: left: typing.Union["Node", int] right: typing.Union["Node", int] @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~11 mins; datastructure explosion: https://github.com/pschanely/hypothesis-crosshair/issues/27", ) @given(st.builds(Node)) def test_can_resolve_recursive_dataclass(val): assert isinstance(val, Node) def test_can_register_new_type_for_typeddicts(): sentinel = object() with temp_registered(C, st.just(sentinel)): assert_simple_property(st.from_type(C), lambda v: v is sentinel) @pytest.mark.parametrize( "lam,source", [ ((lambda a, /, b: a), "lambda a, /, b: a"), ((lambda a=None, /, b=None: a), "lambda a=None, /, b=None: a"), ], ) def test_posonly_lambda_formatting(lam, source): # Testing posonly lambdas, with and without default values assert get_pretty_function_description(lam) == source def test_does_not_convert_posonly_to_keyword(): args, kws = convert_positional_arguments(lambda x, /: None, (1,), {}) assert args assert not kws @given(x=st.booleans()) def test_given_works_with_keyword_only_params(*, x): pass def test_given_works_with_keyword_only_params_some_unbound(): @given(x=st.booleans()) def test(*, x, y): assert y is None test(y=None) def test_given_works_with_positional_only_params(): @given(y=st.booleans()) def test(x, /, y): pass test(None) def test_cannot_pass_strategies_by_position_if_there_are_posonly_args(): @given(st.booleans()) def test(x, /, y): pass with pytest.raises(InvalidArgument): test(None) @fails_with(InvalidArgument) @given(st.booleans()) def test_cannot_pass_strategies_for_posonly_args(x, /): pass @given(y=st.booleans()) def has_posonly_args(x, /, y): pass @pytest.mark.xfail( settings.get_current_profile_name() == "threading", reason=( "dynamic @example applications modify the shared " "has_posonly_args.hypothesis._given_kwargs." ), strict=False, ) def test_example_argument_validation(): example(y=None)(has_posonly_args)(1) # Basic case is OK with pytest.raises( InvalidArgument, match=re.escape( "Cannot pass positional arguments to @example() when decorating " "a test function which has positional-only parameters." ), ): example(None)(has_posonly_args)(1) with pytest.raises( InvalidArgument, match=re.escape( "Inconsistent args: @given() got strategies for 'y', " "but @example() got arguments for 'x'" ), ): example(x=None)(has_posonly_args)(1) class FooProtocol(typing.Protocol): def frozzle(self, x): pass class BarProtocol(typing.Protocol): def bazzle(self, y): pass @given(st.data()) def test_can_resolve_registered_protocol(data): with temp_registered( FooProtocol, st.builds(SimpleNamespace, frozzle=st.functions(like=lambda x: ...)), ): obj = data.draw(st.from_type(FooProtocol)) assert obj.frozzle(x=1) is None def test_cannot_resolve_un_registered_protocol(): msg = "Instance and class checks can only be used with @runtime_checkable protocols" with pytest.raises(TypeError, match=msg): check_can_generate_examples(st.from_type(BarProtocol)) ================================================ FILE: hypothesis-python/tests/cover/test_lookup_py39.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import collections.abc import dataclasses import sys import typing import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument from tests.common.debug import ( assert_all_examples, assert_simple_property, check_can_generate_examples, find_any, ) from tests.common.utils import temp_registered # Union[A, B] is not equivalent to A | B until 3.14. We'll continue to test both # until then. # ruff: noqa: UP007 @pytest.mark.parametrize( "annotated_type,expected_strategy_repr", [ (typing.Annotated[int, "foo"], "integers()"), (typing.Annotated[list[float], "foo"], "lists(floats())"), (typing.Annotated[typing.Annotated[str, "foo"], "bar"], "text()"), ( typing.Annotated[typing.Annotated[list[dict[str, bool]], "foo"], "bar"], "lists(dictionaries(keys=text(), values=booleans()))", ), ], ) def test_typing_Annotated(annotated_type, expected_strategy_repr): assert repr(st.from_type(annotated_type)) == expected_strategy_repr PositiveInt = typing.Annotated[int, st.integers(min_value=1)] MoreThanTenInt = typing.Annotated[PositiveInt, st.integers(min_value=10 + 1)] WithTwoStrategies = typing.Annotated[int, st.integers(), st.none()] ExtraAnnotationNoStrategy = typing.Annotated[PositiveInt, "metadata"] def arg_positive(x: PositiveInt): assert x > 0 def arg_more_than_ten(x: MoreThanTenInt): assert x > 10 @given(st.data()) def test_annotated_positive_int(data): data.draw(st.builds(arg_positive)) @given(st.data()) def test_annotated_more_than_ten(data): data.draw(st.builds(arg_more_than_ten)) @given(st.data()) def test_annotated_with_two_strategies(data): assert data.draw(st.from_type(WithTwoStrategies)) is None @given(st.data()) def test_annotated_extra_metadata(data): assert data.draw(st.from_type(ExtraAnnotationNoStrategy)) > 0 @dataclasses.dataclass class User: id: int following: list["User"] # works with typing.List @pytest.mark.skipif(sys.version_info[:2] >= (3, 11), reason="works in new Pythons") def test_string_forward_ref_message(): # See https://github.com/HypothesisWorks/hypothesis/issues/3016 s = st.builds(User) with pytest.raises(InvalidArgument, match="`from __future__ import annotations`"): check_can_generate_examples(s) @pytest.mark.parametrize("typ", (typing.Union[list[int], int], list[int] | int)) def test_issue_3080(typ): # Check for https://github.com/HypothesisWorks/hypothesis/issues/3080 s = st.from_type(typ) find_any(s, lambda x: isinstance(x, int)) find_any(s, lambda x: isinstance(x, list)) @dataclasses.dataclass class TypingTuple: a: dict[tuple[int, int], str] @dataclasses.dataclass class BuiltinTuple: a: dict[tuple[int, int], str] TestDataClass = typing.Union[TypingTuple, BuiltinTuple] @pytest.mark.parametrize("data_class", [TypingTuple, BuiltinTuple]) @given(data=st.data()) def test_from_type_with_tuple_works(data, data_class: TestDataClass): value: TestDataClass = data.draw(st.from_type(data_class)) assert len(value.a) >= 0 def _shorter_lists(list_type): return st.lists(st.from_type(*typing.get_args(list_type)), max_size=2) def test_can_register_builtin_list(): # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/3635 with temp_registered(list, _shorter_lists): assert_all_examples( st.from_type(list[int]), lambda ls: len(ls) <= 2 and {type(x) for x in ls}.issubset({int}), ) T = typing.TypeVar("T") @typing.runtime_checkable class Fooable(typing.Protocol[T]): def foo(self): ... class FooableConcrete(tuple): def foo(self): pass def test_only_tuple_subclasses_in_typing_type(): # A generic typing type (such as Fooable) whose only concrete # instantiations are tuples should still generate tuples. This is in # contrast to test_tuple_subclasses_not_generic_sequences, which discards # tuples if there are any alternatives. with temp_registered(FooableConcrete, st.builds(FooableConcrete)): s = st.from_type(Fooable[int]) assert_all_examples(s, lambda x: type(x) is FooableConcrete) def test_lookup_registered_tuple(): sentinel = object() typ = tuple[int] with temp_registered(tuple, st.just(sentinel)): assert_simple_property(st.from_type(typ), lambda v: v is sentinel) assert_simple_property(st.from_type(typ), lambda v: v is not sentinel) sentinel = object() class LazyStrategyAnnotation: __is_annotated_types_grouped_metadata__ = True def __iter__(self): return iter([st.just(sentinel)]) @given(...) def test_grouped_protocol_strategy(x: typing.Annotated[int, LazyStrategyAnnotation()]): assert x is sentinel def test_collections_abc_callable_none(): # https://github.com/HypothesisWorks/hypothesis/issues/4192 s = st.from_type(collections.abc.Callable[[None], None]) assert_all_examples(s, lambda x: callable(x) and x(None) is None) ================================================ FILE: hypothesis-python/tests/cover/test_map.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import assume, given, strategies as st from hypothesis.strategies._internal.lazy import unwrap_strategies from tests.common.debug import assert_no_examples @given(st.integers().map(lambda x: assume(x % 3 != 0) and x)) def test_can_assume_in_map(x): assert x % 3 != 0 def test_assume_in_just_raises_immediately(): assert_no_examples(st.just(1).map(lambda x: assume(x == 2))) def test_identity_map_is_noop(): s = unwrap_strategies(st.integers()) assert s.map(lambda x: x) is s ================================================ FILE: hypothesis-python/tests/cover/test_mock.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Checks that @given, @mock.patch, and pytest fixtures work as expected.""" import math from unittest import mock try: from pytest import Config except ImportError: # pytest<7.0.0 from _pytest.config import Config from hypothesis import given, strategies as st from tests.common.utils import skipif_threading @given(thing=st.text()) @mock.patch("math.atan") # mock.patch is thread-unsafe. pytest-run-parallel normally detects this and skips # the test, but because we set it inside of @given it can't (or doesn't?) peek # inside the AST, resulting in a false negative. @skipif_threading def test_can_mock_inside_given_without_fixture(atan, thing): assert isinstance(atan, mock.MagicMock) assert isinstance(math.atan, mock.MagicMock) @mock.patch("math.atan") @given(thing=st.text()) def test_can_mock_outside_given_with_fixture(atan, pytestconfig, thing): assert isinstance(atan, mock.MagicMock) assert isinstance(math.atan, mock.MagicMock) assert isinstance(pytestconfig, Config) @given(thing=st.text()) def test_can_mock_within_test_with_fixture(pytestconfig, thing): assert isinstance(pytestconfig, Config) assert not isinstance(math.atan, mock.MagicMock) with mock.patch("math.atan") as atan: assert isinstance(atan, mock.MagicMock) assert isinstance(math.atan, mock.MagicMock) assert not isinstance(math.atan, mock.MagicMock) ================================================ FILE: hypothesis-python/tests/cover/test_monitoring.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys from contextlib import contextmanager import pytest from hypothesis import given, strategies as st from hypothesis.errors import HypothesisWarning from hypothesis.internal.scrutineer import MONITORING_TOOL_ID @contextmanager def using_tool_id(tool_id, tool_name): try: sys.monitoring.use_tool_id(tool_id, tool_name) yield finally: sys.monitoring.free_tool_id(tool_id) @pytest.mark.skipif(sys.version_info[:2] < (3, 12), reason="new namespace") def test_monitoring_warns_on_registered_tool_id(warns_or_raises): # scrutineer can't run if something has already registered its tool id. with ( using_tool_id(MONITORING_TOOL_ID, "rogue"), warns_or_raises(HypothesisWarning, match=r"already taken by tool rogue"), ): @given(st.integers()) def f(n): raise AssertionError with pytest.raises(AssertionError): f() ================================================ FILE: hypothesis-python/tests/cover/test_nothing.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument from tests.common.debug import assert_no_examples, minimal def test_resampling(): x = minimal( st.lists(st.integers(), min_size=1).flatmap( lambda x: st.lists(st.sampled_from(x)) ), lambda x: len(x) >= 10 and len(set(x)) == 1, ) assert x == [0] * 10 @given(st.lists(st.nothing())) def test_list_of_nothing(xs): assert xs == [] @given(st.sets(st.nothing())) def test_set_of_nothing(xs): assert xs == set() def test_validates_min_size(): with pytest.raises(InvalidArgument): st.lists(st.nothing(), min_size=1).validate() def test_function_composition(): assert st.nothing().map(lambda x: "hi").is_empty assert st.nothing().filter(lambda x: True).is_empty assert st.nothing().flatmap(lambda x: st.integers()).is_empty def test_tuples_detect_empty_elements(): assert st.tuples(st.nothing()).is_empty def test_fixed_dictionaries_detect_empty_values(): assert st.fixed_dictionaries({"a": st.nothing()}).is_empty def test_no_examples(): assert_no_examples(st.nothing()) @pytest.mark.parametrize( "s", [ st.nothing(), st.nothing().map(lambda x: x), st.nothing().filter(lambda x: True), st.nothing().flatmap(lambda x: st.integers()), ], ) def test_empty(s): assert s.is_empty ================================================ FILE: hypothesis-python/tests/cover/test_numerics.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import decimal from math import copysign, inf import pytest from hypothesis import HealthCheck, assume, given, reject, settings from hypothesis.errors import InvalidArgument from hypothesis.internal.floats import next_down, next_up from hypothesis.strategies import ( booleans, data, decimals, floats, fractions, integers, none, sampled_from, tuples, ) from tests.common.debug import check_can_generate_examples, find_any, minimal from tests.common.utils import checks_deprecated_behaviour, skipif_threading @settings(suppress_health_check=list(HealthCheck)) @given(data()) def test_fuzz_floats_bounds(data): width = data.draw(sampled_from([64, 32, 16])) bound = none() | floats(allow_nan=False, width=width) low, high = data.draw(tuples(bound, bound), label="low, high") if low is not None and high is not None and low > high: low, high = high, low if low is not None and high is not None and low > high: low, high = high, low exmin = low is not None and low != inf and data.draw(booleans(), label="ex_min") exmax = high is not None and high != -inf and data.draw(booleans(), label="ex_max") if low is not None and high is not None: lo = next_up(low, width) if exmin else low hi = next_down(high, width) if exmax else high # There must actually be floats between these bounds assume(lo <= hi) if lo == hi == 0: assume(not exmin and not exmax and copysign(1.0, lo) <= copysign(1.0, hi)) s = floats(low, high, exclude_min=exmin, exclude_max=exmax, width=width) val = data.draw(s, label="value") assume(val) # positive/negative zero is an issue if low is not None: assert low <= val if high is not None: assert val <= high if exmin: assert low != val if exmax: assert high != val @given(data()) def test_fuzz_fractions_bounds(data): denom = data.draw(none() | integers(1, 100), label="denominator") fracs = none() | fractions(max_denominator=denom) low, high = data.draw(tuples(fracs, fracs), label="low, high") if low is not None and high is not None and low > high: low, high = high, low try: val = data.draw(fractions(low, high, max_denominator=denom), label="value") except InvalidArgument: reject() # fractions too close for given max_denominator if low is not None: assert low <= val if high is not None: assert val <= high if denom is not None: assert 1 <= val.denominator <= denom @given(data()) def test_fuzz_decimals_bounds(data): places = data.draw(none() | integers(0, 20), label="places") finite_decs = ( decimals(allow_nan=False, allow_infinity=False, places=places) | none() ) low, high = data.draw(tuples(finite_decs, finite_decs), label="low, high") if low is not None and high is not None and low > high: low, high = high, low ctx = decimal.Context(prec=data.draw(integers(1, 100), label="precision")) try: with decimal.localcontext(ctx): strat = decimals( low, high, allow_nan=False, allow_infinity=False, places=places ) val = data.draw(strat, label="value") except InvalidArgument: reject() # decimals too close for given places if low is not None: assert low <= val if high is not None: assert val <= high if places is not None: assert val.as_tuple().exponent == -places def test_all_decimals_can_be_exact_floats(): find_any( decimals(), lambda x: assume(x.is_finite()) and decimal.Decimal(float(x)) == x ) @given(fractions(), fractions(), fractions()) def test_fraction_addition_is_well_behaved(x, y, z): assert x + y + z == y + x + z def test_decimals_include_nan(): find_any(decimals(), lambda x: x.is_nan()) def test_decimals_include_inf(): find_any(decimals(), lambda x: x.is_infinite(), settings(max_examples=10**6)) @given(decimals(allow_nan=False)) def test_decimals_can_disallow_nan(x): assert not x.is_nan() @given(decimals(allow_infinity=False)) def test_decimals_can_disallow_inf(x): assert not x.is_infinite() @pytest.mark.parametrize("places", range(10)) def test_decimals_have_correct_places(places): @given(decimals(0, 10, allow_nan=False, places=places)) def inner_tst(n): assert n.as_tuple().exponent == -places inner_tst() @given(decimals(min_value="0.1", max_value="0.2", allow_nan=False, places=1)) def test_works_with_few_values(dec): assert dec in (decimal.Decimal("0.1"), decimal.Decimal("0.2")) @given(decimals(places=3, allow_nan=False, allow_infinity=False)) def test_issue_725_regression(x): pass @given(decimals(min_value="0.1", max_value="0.3")) def test_issue_739_regression(x): pass def test_consistent_decimal_error(): bad = "invalid argument to Decimal" with pytest.raises(InvalidArgument) as excinfo: check_can_generate_examples(decimals(bad)) with ( pytest.raises(InvalidArgument) as excinfo2, decimal.localcontext(decimal.Context(traps=[])), ): check_can_generate_examples(decimals(bad)) assert str(excinfo.value) == str(excinfo2.value) @pytest.mark.parametrize( "s, msg", [ ( floats(min_value=inf, allow_infinity=False), "allow_infinity=False excludes min_value=inf", ), ( floats(min_value=next_down(inf), exclude_min=True, allow_infinity=False), "exclude_min=True turns min_value=.+? into inf, but allow_infinity=False", ), ( floats(max_value=-inf, allow_infinity=False), "allow_infinity=False excludes max_value=-inf", ), ( floats(max_value=next_up(-inf), exclude_max=True, allow_infinity=False), "exclude_max=True turns max_value=.+? into -inf, but allow_infinity=False", ), ], ) @skipif_threading # strategy instances are shared, which persists .validate_called def test_floats_message(s, msg): # https://github.com/HypothesisWorks/hypothesis/issues/3207 with pytest.raises(InvalidArgument, match=msg): s.validate() @pytest.mark.parametrize("s", [decimals(), decimals(places=10)], ids=repr) def test_minimal_nonfinite_decimal_is_inf(s): assert minimal(s.filter(lambda x: not x.is_finite())) == decimal.Decimal("Infinity") @checks_deprecated_behaviour def test_decimals_warns_for_inexact_numeric_bounds(): check_can_generate_examples(decimals(min_value=1e-100)) ================================================ FILE: hypothesis-python/tests/cover/test_observability.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import base64 import contextlib import json import math import textwrap import threading import warnings from collections import defaultdict from contextlib import nullcontext import pytest import hypothesis.internal.observability from hypothesis import ( assume, event, example, given, note, seed, settings, strategies as st, target, ) from hypothesis.database import InMemoryExampleDatabase from hypothesis.internal.compat import PYPY from hypothesis.internal.conjecture.choice import ChoiceNode, choices_key from hypothesis.internal.conjecture.data import Span from hypothesis.internal.coverage import IN_COVERAGE_TESTS from hypothesis.internal.floats import SIGNALING_NAN, float_to_int, int_to_float from hypothesis.internal.intervalsets import IntervalSet from hypothesis.internal.observability import ( TESTCASE_CALLBACKS, InfoObservation, TestCaseObservation, add_observability_callback, choices_to_json, nodes_to_json, observability_enabled, remove_observability_callback, with_observability_callback, ) from hypothesis.stateful import ( RuleBasedStateMachine, invariant, rule, run_state_machine_as_test, ) from hypothesis.strategies._internal.utils import to_jsonable from tests.common.utils import ( Why, capture_observations, checks_deprecated_behaviour, run_concurrently, skipif_threading, xfail_on_crosshair, ) from tests.conjecture.common import choices, integer_constr, nodes @seed("deterministic so we don't miss some combination of features") @example(l=[1], a=0, x=4, data=None) # explicitly set max_examples=100 to override our lower example limit for coverage tests. @settings(database=InMemoryExampleDatabase(), deadline=None, max_examples=100) @given(st.lists(st.integers()), st.integers(), st.integers(), st.data()) def do_it_all(l, a, x, data): event(f"{x%2=}") target(x % 5, label="x%5") assume(a % 9) assume(len(l) > 0) if data: data.draw(st.text("abcdef", min_size=a % 3), label="interactive") 1 / ((x or 1) % 7) @xfail_on_crosshair(Why.other, strict=False) # flakey BackendCannotProceed ?? @skipif_threading # captures observations from other threads def test_observability(): with capture_observations() as ls: with pytest.raises(ZeroDivisionError): do_it_all() with pytest.raises(ZeroDivisionError): do_it_all() infos = [t for t in ls if t.type == "info"] assert len(infos) == 2 assert {t.title for t in infos} == {"Hypothesis Statistics"} testcases = [t for t in ls if t.type == "test_case"] assert len(testcases) > 50 assert {t.property for t in testcases} == {do_it_all.__name__} assert len({t.run_start for t in testcases}) == 2 assert {t.status for t in testcases} == {"gave_up", "passed", "failed"} for t in testcases: if t.status != "gave_up": assert t.timing assert ("interactive" in t.arguments) == ( "generate:interactive" in t.timing ) @xfail_on_crosshair(Why.other) def test_capture_unnamed_arguments(): @given(st.integers(), st.floats(), st.data()) def f(v1, v2, data): data.draw(st.booleans()) with capture_observations() as observations: f() test_cases = [tc for tc in observations if tc.type == "test_case"] for test_case in test_cases: assert list(test_case.arguments.keys()) == [ "v1", "v2", "data", "Draw 1", ], test_case @pytest.mark.skipif( PYPY or IN_COVERAGE_TESTS, reason="coverage requires sys.settrace pre-3.12" ) def test_failure_includes_explain_phase_comments(): @given(st.integers(), st.integers()) @settings(database=None) def test_fails(x, y): if x: raise AssertionError with capture_observations() as observations, pytest.raises(AssertionError): test_fails() test_cases = [tc for tc in observations if tc.type == "test_case"] # only the last test case observation, once we've finished shrinking it, # will include explain phase comments. # # Note that the output does *not* include `Explanation:` comments. See # https://github.com/HypothesisWorks/hypothesis/pull/4399#discussion_r2101559648 expected = textwrap.dedent( r""" test_fails( x=1, y=0, # or any other generated value ) """ ).strip() assert test_cases[-1].representation == expected def test_failure_includes_notes(): @given(st.data()) @settings(database=None) def test_fails_with_note(data): note("not included 1") data.draw(st.booleans()) note("not included 2") raise AssertionError with capture_observations() as observations, pytest.raises(AssertionError): test_fails_with_note() expected = textwrap.dedent( """ test_fails_with_note( data=data(...), ) Draw 1: False """ ).strip() test_cases = [tc for tc in observations if tc.type == "test_case"] assert test_cases[-1].representation == expected def test_normal_representation_includes_draws(): @given(st.data()) def f(data): b1 = data.draw(st.booleans()) note("not included") b2 = data.draw(st.booleans(), label="second") assume(b1 and b2) with capture_observations() as observations: f() crosshair = settings.get_current_profile_name() == "crosshair" expected = textwrap.dedent( f""" f( data={'' if crosshair else 'data(...)'}, ) Draw 1: True Draw 2 (second): True """ ).strip() test_cases = [ tc for tc in observations if tc.type == "test_case" and tc.status == "passed" ] assert test_cases # TODO crosshair has a soundness bug with assume. remove branch when fixed # https://github.com/pschanely/hypothesis-crosshair/issues/34 if not crosshair: assert {tc.representation for tc in test_cases} == {expected} @xfail_on_crosshair(Why.other) def test_capture_named_arguments(): @given(named1=st.integers(), named2=st.floats(), data=st.data()) def f(named1, named2, data): data.draw(st.booleans()) with capture_observations() as observations: f() test_cases = [tc for tc in observations if tc.type == "test_case"] for test_case in test_cases: assert list(test_case.arguments.keys()) == [ "named1", "named2", "data", "Draw 1", ], test_case def test_assume_has_status_reason(): @given(st.booleans()) def f(b): assume(b) with capture_observations() as ls: f() gave_ups = [t for t in ls if t.type == "test_case" and t.status == "gave_up"] for gave_up in gave_ups: assert gave_up.status_reason.startswith("failed to satisfy assume() in f") @pytest.mark.skipif( PYPY or IN_COVERAGE_TESTS, reason="coverage requires sys.settrace pre-3.12" ) def test_minimal_failing_observation(): @given(st.integers(), st.integers()) @settings(database=None) def test_fails(x, y): if x: raise AssertionError with capture_observations() as observations, pytest.raises(AssertionError): test_fails() observation = [tc for tc in observations if tc.type == "test_case"][-1] expected_representation = textwrap.dedent( r""" test_fails( x=1, y=0, # or any other generated value ) """ ).strip() assert observation.type == "test_case" assert observation.property == "test_fails" assert observation.status == "failed" assert "AssertionError" in observation.status_reason assert set(observation.timing.keys()) == { "execute:test", "overall:gc", "generate:x", "generate:y", } assert observation.coverage is None assert observation.features == {} assert observation.how_generated == "minimal failing example" assert "AssertionError" in observation.metadata.traceback assert "test_fails" in observation.metadata.traceback assert observation.metadata.reproduction_decorator.startswith("@reproduce_failure") assert observation.representation == expected_representation assert observation.arguments == {"x": 1, "y": 0} @pytest.mark.skipif( PYPY or IN_COVERAGE_TESTS, reason="coverage requires sys.settrace pre-3.12" ) def test_all_failing_observations_have_reproduction_decorator(): @given(st.integers()) def test_fails(x): raise AssertionError with capture_observations() as observations, pytest.raises(AssertionError): test_fails() # all failed test case observations should have reprodution_decorator for observation in [ tc for tc in observations if tc.type == "test_case" and tc.status == "failed" ]: decorator = observation.metadata.reproduction_decorator assert decorator is not None assert decorator.startswith("@reproduce_failure") @settings(max_examples=20, stateful_step_count=5) class UltraSimpleMachine(RuleBasedStateMachine): value = 0 @rule() def inc(self): self.value += 1 @rule() def dec(self): self.value -= 1 @invariant() def limits(self): assert abs(self.value) <= 100 @xfail_on_crosshair(Why.other, strict=False) def test_observability_captures_stateful_reprs(): with capture_observations() as ls: run_state_machine_as_test(UltraSimpleMachine) for x in ls: if x.type != "test_case" or x.status == "gave_up": continue r = x.representation assert "state.limits()" in r assert "state.inc()" in r or "state.dec()" in r # or both t = x.timing assert "execute:invariant:limits" in t has_inc = "generate:rule:inc" in t and "execute:rule:inc" in t has_dec = "generate:rule:dec" in t and "execute:rule:dec" in t assert has_inc or has_dec # BytestringProvider.draw_boolean divides [0, 127] as False and [128, 255] # as True @pytest.mark.parametrize( "buffer, expected_status", [ # Status.OVERRUN (b"", "gave_up"), # Status.INVALID (b"\x00" + bytes([255]), "gave_up"), # Status.VALID (b"\x00\x00", "passed"), # Status.INTERESTING (bytes([255]) + b"\x00", "failed"), ], ) def test_fuzz_one_input_status(buffer, expected_status): @given(st.booleans(), st.booleans()) def test_fails(should_fail, should_fail_assume): if should_fail: raise AssertionError if should_fail_assume: assume(False) with ( capture_observations() as ls, pytest.raises(AssertionError) if expected_status == "failed" else nullcontext(), ): test_fails.hypothesis.fuzz_one_input(buffer) assert len(ls) == 1 assert ls[0].status == expected_status assert ls[0].how_generated == "fuzz_one_input" def _decode_choice(value): if isinstance(value, list): if value[0] == "integer": # large integers get cast to string, stored as ["integer", str(value)] assert isinstance(value[1], str) return int(value[1]) elif value[0] == "bytes": assert isinstance(value[1], str) return base64.b64decode(value[1]) elif value[0] == "float": assert isinstance(value[1], int) choice = int_to_float(value[1]) assert math.isnan(choice) return choice else: return value[1] return value def _decode_choices(data): return [_decode_choice(value) for value in data] def _decode_nodes(data): return [ ChoiceNode( type=node["type"], value=_decode_choice(node["value"]), constraints=_decode_constraints(node["type"], node["constraints"]), was_forced=node["was_forced"], ) for node in data ] def _decode_constraints(choice_type, data): if choice_type == "integer": return { "min_value": _decode_choice(data["min_value"]), "max_value": _decode_choice(data["max_value"]), "weights": ( None if data["weights"] is None else {_decode_choice(k): v for k, v in data["weights"]} ), "shrink_towards": _decode_choice(data["shrink_towards"]), } elif choice_type == "float": return { "min_value": _decode_choice(data["min_value"]), "max_value": _decode_choice(data["max_value"]), "allow_nan": data["allow_nan"], "smallest_nonzero_magnitude": data["smallest_nonzero_magnitude"], } elif choice_type == "string": return { "intervals": IntervalSet(tuple(data["intervals"])), "min_size": _decode_choice(data["min_size"]), "max_size": _decode_choice(data["max_size"]), } elif choice_type == "bytes": return { "min_size": _decode_choice(data["min_size"]), "max_size": _decode_choice(data["max_size"]), } elif choice_type == "boolean": return {"p": data["p"]} else: raise ValueError(f"unknown choice type {choice_type}") def _has_surrogate(choice): return isinstance(choice, str) and any(0xD800 <= ord(c) <= 0xDFFF for c in choice) @example([0.0]) @example([-0.0]) @example([SIGNALING_NAN]) @example([math.nan]) @example([math.inf]) @example([-math.inf]) # json.{loads, dumps} does not roundtrip for surrogate pairs; they are combined # into the single code point by json.loads: # json.loads(json.dumps("\udbf4\udc00")) == '\U0010d000' # # Ignore this case with an `assume`, and add an explicit example to ensure we # continue to do so. @example(["\udbf4\udc00"]) @given(st.lists(choices())) def test_choices_json_roundtrips(choices): assume(not any(_has_surrogate(choice) for choice in choices)) choices2 = _decode_choices(json.loads(json.dumps(choices_to_json(choices)))) assert choices_key(choices) == choices_key(choices2) @given(st.lists(nodes())) def test_nodes_json_roundtrips(nodes): assume( not any( _has_surrogate(node.value) or any(_has_surrogate(value) for value in node.constraints.values()) for node in nodes ) ) nodes2 = _decode_nodes(json.loads(json.dumps(nodes_to_json(nodes)))) assert nodes == nodes2 @pytest.mark.parametrize( "choice, expected", [ (math.nan, ["float", float_to_int(math.nan)]), (SIGNALING_NAN, ["float", float_to_int(SIGNALING_NAN)]), (1, 1), (-1, -1), (2**63 + 1, ["integer", str(2**63 + 1)]), (-(2**63 + 1), ["integer", str(-(2**63 + 1))]), (1.0, 1.0), (-0.0, -0.0), (0.0, 0.0), (True, True), (False, False), (b"a", ["bytes", "YQ=="]), ], ) def test_choices_to_json_explicit(choice, expected): assert choices_to_json([choice]) == [expected] @pytest.mark.parametrize( "choice_node, expected", [ ( ChoiceNode( type="integer", value=2**63 + 1, constraints=integer_constr(), was_forced=False, ), { "type": "integer", "value": ["integer", str(2**63 + 1)], "constraints": integer_constr(), "was_forced": False, }, ), ], ) def test_choice_nodes_to_json_explicit(choice_node, expected): assert nodes_to_json([choice_node]) == [expected] def test_metadata_to_json(): # this is mostly a covering test than testing anything particular about # ObservationMetadata. @given(st.integers()) def f(n): pass with capture_observations(choices=True) as observations: f() observations = [obs for obs in observations if obs.type == "test_case"] for observation in observations: assert set( to_jsonable(observation.metadata, avoid_realization=False).keys() ) == { "traceback", "reproduction_decorator", "predicates", "backend", "sys.argv", "os.getpid()", "imported_at", "data_status", "phase", "interesting_origin", "choice_nodes", "choice_spans", } assert observation.metadata.choice_nodes is not None for span in observation.metadata.choice_spans: assert isinstance(span, Span) assert 0 <= span.start <= len(observation.metadata.choice_nodes) assert 0 <= span.end <= len(observation.metadata.choice_nodes) @contextlib.contextmanager def restore_callbacks(): callbacks = hypothesis.internal.observability._callbacks.copy() callbacks_all = hypothesis.internal.observability._callbacks_all_threads.copy() try: yield finally: hypothesis.internal.observability._callbacks = callbacks hypothesis.internal.observability._callbacks_all_threads = callbacks_all @contextlib.contextmanager def with_collect_coverage(*, value: bool): original_value = hypothesis.internal.observability.OBSERVABILITY_COLLECT_COVERAGE hypothesis.internal.observability.OBSERVABILITY_COLLECT_COVERAGE = value try: yield finally: hypothesis.internal.observability.OBSERVABILITY_COLLECT_COVERAGE = ( original_value ) def _callbacks(): # respect changes from the restore_callbacks context manager by re-accessing # its namespace, instead of keeping # `from hypothesis.internal.observability import _callbacks` around return hypothesis.internal.observability._callbacks @skipif_threading def test_observability_callbacks(): def f(observation): pass def g(observation): pass thread_id = threading.get_ident() with restore_callbacks(): assert not observability_enabled() add_observability_callback(f) assert _callbacks() == {thread_id: [f]} assert observability_enabled() add_observability_callback(g) assert _callbacks() == {thread_id: [f, g]} assert observability_enabled() remove_observability_callback(g) assert _callbacks() == {thread_id: [f]} assert observability_enabled() remove_observability_callback(g) assert _callbacks() == {thread_id: [f]} assert observability_enabled() remove_observability_callback(f) assert _callbacks() == {} assert not observability_enabled() @skipif_threading def test_observability_callbacks_all_threads(): thread_id = threading.get_ident() def f(observation, thread_id): pass with restore_callbacks(): assert not observability_enabled() add_observability_callback(f, all_threads=True) assert hypothesis.internal.observability._callbacks_all_threads == [f] assert _callbacks() == {} assert observability_enabled() add_observability_callback(f) assert hypothesis.internal.observability._callbacks_all_threads == [f] assert _callbacks() == {thread_id: [f]} assert observability_enabled() # remove_observability_callback removes it both from per-thread and # all_threads. The semantics of duplicated callbacks is weird enough # that I don't want to commit to anything here, so I'm leaving this as # somewhat undefined behavior, and recommending that users simply not # register a callback both normally and for all threads. remove_observability_callback(f) assert hypothesis.internal.observability._callbacks_all_threads == [] assert _callbacks() == {} assert not observability_enabled() @checks_deprecated_behaviour def test_testcase_callbacks_deprecation_bool(): bool(TESTCASE_CALLBACKS) @checks_deprecated_behaviour def test_testcase_callbacks_deprecation_append(): with restore_callbacks(): TESTCASE_CALLBACKS.append(lambda x: None) @checks_deprecated_behaviour def test_testcase_callbacks_deprecation_remove(): with restore_callbacks(): TESTCASE_CALLBACKS.remove(lambda x: None) def test_testcase_callbacks(): def f(observation): pass def g(observation): pass thread_id = threading.get_ident() with restore_callbacks(), warnings.catch_warnings(): # ignore TESTCASE_CALLBACKS deprecation warnings warnings.simplefilter("ignore") assert not bool(TESTCASE_CALLBACKS) add_observability_callback(f) assert _callbacks() == {thread_id: [f]} assert bool(TESTCASE_CALLBACKS) add_observability_callback(g) assert _callbacks() == {thread_id: [f, g]} assert bool(TESTCASE_CALLBACKS) remove_observability_callback(g) assert _callbacks() == {thread_id: [f]} assert bool(TESTCASE_CALLBACKS) remove_observability_callback(f) assert _callbacks() == {} assert not bool(TESTCASE_CALLBACKS) def test_only_receives_callbacks_from_this_thread(): @given(st.integers()) def g(n): pass def test(): count_observations = 0 def callback(observation): nonlocal count_observations count_observations += 1 add_observability_callback(callback) with warnings.catch_warnings(): g() # one per example, plus one for the overall run assert count_observations == settings().max_examples + 1 with ( restore_callbacks(), # Observability tries to record coverage, but we don't currently # support concurrent coverage collection, and issue a warning instead. # # I tried to fix this with: # # warnings.filterwarnings( # "ignore", message=r".*tool id \d+ is already taken by tool scrutineer.*" # ) # # but that had a race condition somehow and sometimes still didn't work?? The # warnings module is not thread-safe until 3.14, I think. with_collect_coverage(value=False), ): run_concurrently(test, n=5) def test_all_threads_callback(): n_threads = 5 # thread_id: count calls = defaultdict(int) def global_callback(observation, thread_id): assert isinstance(observation, (TestCaseObservation, InfoObservation)) assert isinstance(thread_id, int) calls[thread_id] += 1 @given(st.integers()) def f(n): pass with ( with_collect_coverage(value=False), with_observability_callback(global_callback, all_threads=True), ): run_concurrently(f, n=n_threads) assert len(calls) == n_threads assert all(count == (settings().max_examples + 1) for count in calls.values()) ================================================ FILE: hypothesis-python/tests/cover/test_one_of.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re from collections.abc import Sequence import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument from tests.common.debug import assert_no_examples def test_one_of_empty(): e = st.one_of() assert e.is_empty assert_no_examples(e) @given(st.one_of(st.integers().filter(bool))) def test_one_of_filtered(i): assert bool(i) @given(st.one_of(st.just(100).flatmap(st.integers))) def test_one_of_flatmapped(i): assert i >= 100 def test_one_of_single_strategy_is_noop(): s = st.integers() assert st.one_of(s) is s assert st.one_of([s]) is s def test_one_of_without_strategies_suggests_sampled_from(): with pytest.raises( InvalidArgument, match=re.escape("Did you mean st.sampled_from([1, 2, 3])?"), ): st.one_of(1, 2, 3) @pytest.mark.parametrize( "strategy, count", [ (st.one_of(st.integers(), st.integers(), st.integers()), 1), (st.one_of(st.integers(), st.one_of(st.integers(), st.integers())), 2), ((st.integers() | st.integers()) | st.integers(), 1), (st.integers() | (st.integers() | st.integers()), 1), (st.integers() | st.text() | st.booleans(), 1), (st.from_type(int | Sequence[int]), 2), ], ) def test_one_of_unwrapping(strategy, count): assert repr(strategy).count("one_of(") == count ================================================ FILE: hypothesis-python/tests/cover/test_permutations.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given from hypothesis.errors import InvalidArgument from hypothesis.strategies import permutations from tests.common.debug import check_can_generate_examples, minimal from tests.common.utils import fails_with def test_can_find_non_trivial_permutation(): x = minimal(permutations(list(range(5))), lambda x: x[0] != 0) assert x == [1, 0, 2, 3, 4] @given(permutations(list("abcd"))) def test_permutation_values_are_permutations(perm): assert len(perm) == 4 assert set(perm) == set("abcd") @given(permutations([])) def test_empty_permutations_are_empty(xs): assert xs == [] @fails_with(InvalidArgument) def test_cannot_permute_non_sequence_types(): check_can_generate_examples(permutations(set())) ================================================ FILE: hypothesis-python/tests/cover/test_phases.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import Phase, example, given, settings, strategies as st from hypothesis.database import ExampleDatabase, InMemoryExampleDatabase from hypothesis.errors import InvalidArgument from tests.common.utils import skipif_emscripten @example(11) @settings(phases=(Phase.explicit,)) @given(st.integers()) def test_only_runs_explicit_examples(i): assert i == 11 @example("hello world") @settings(phases=(Phase.reuse, Phase.generate, Phase.shrink)) @given(st.booleans()) def test_does_not_use_explicit_examples(i): assert isinstance(i, bool) @skipif_emscripten # TODO: actually Pytest 9.0, remove this workaround ASAP @settings(phases=(Phase.reuse, Phase.shrink), database=InMemoryExampleDatabase()) @given(st.booleans()) def test_this_would_fail_if_you_ran_it(b): raise AssertionError @pytest.mark.parametrize( "arg,expected", [ (tuple(Phase)[::-1], tuple(Phase)), ([Phase.explicit, Phase.explicit], (Phase.explicit,)), ], ) def test_sorts_and_dedupes_phases(arg, expected): assert settings(phases=arg).phases == expected def test_phases_default_to_all(): assert settings().phases == tuple(Phase) def test_does_not_reuse_saved_examples_if_reuse_not_in_phases(): class BadDatabase(ExampleDatabase): def save(self, key, value): pass def delete(self, key, value): pass def fetch(self, key): raise ValueError def close(self): pass @settings(database=BadDatabase(), phases=(Phase.generate,)) @given(st.integers()) def test_usage(i): pass test_usage() def test_will_save_when_reuse_not_in_phases(): database = InMemoryExampleDatabase() assert not database.data @settings(database=database, phases=(Phase.generate,)) @given(st.integers()) def test_usage(i): raise ValueError with pytest.raises(ValueError): test_usage() (saved,) = (v for k, v in database.data.items() if b"pareto" not in k) assert len(saved) == 1 def test_rejects_non_phases(): with pytest.raises(InvalidArgument): settings(phases=["cabbage"]) ================================================ FILE: hypothesis-python/tests/cover/test_posonly_args_py38.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, strategies as st @st.composite def strat(draw, x=0, /): return draw(st.integers(min_value=x)) @given(st.data(), st.integers()) def test_composite_with_posonly_args(data, min_value): v = data.draw(strat(min_value)) assert min_value <= v def test_preserves_signature(): with pytest.raises(TypeError): strat(x=1) def test_builds_real_pos_only(): with pytest.raises(TypeError): st.builds() # requires a target! ================================================ FILE: hypothesis-python/tests/cover/test_pretty.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """This file originates in the IPython project and is made use of under the following licensing terms: The IPython licensing terms IPython is licensed under the terms of the Modified BSD License (also known as New or Revised or 3-Clause BSD), as follows: Copyright (c) 2008-2014, IPython Development Team Copyright (c) 2001-2007, Fernando Perez Copyright (c) 2001, Janko Hauser Copyright (c) 2001, Nathaniel Gray All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the IPython Development Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import io import re import struct import warnings from collections import Counter, OrderedDict, defaultdict, deque from dataclasses import dataclass, field from enum import Enum, Flag from functools import partial import pytest from hypothesis import given, strategies as st from hypothesis.control import current_build_context from hypothesis.internal.compat import PYPY from hypothesis.internal.conjecture.floats import float_to_lex from hypothesis.internal.floats import SIGNALING_NAN from hypothesis.vendor import pretty class MyList: def __init__(self, content): self.content = content def _repr_pretty_(self, p, cycle): if cycle: p.text("MyList(...)") else: with p.group(3, "MyList(", ")"): for i, child in enumerate(self.content): if i: p.text(",") p.breakable() else: p.breakable("") p.pretty(child) class MyDict(dict): def _repr_pretty_(self, p, cycle): p.text("MyDict(...)") class MyObj: def somemethod(self): pass class Dummy1: def _repr_pretty_(self, p, cycle): p.text("Dummy1(...)") class Dummy2: _repr_pretty_ = None def __repr__(self): return "Dummy2()" class NoModule: pass NoModule.__module__ = None class Breaking: def _repr_pretty_(self, p, cycle): with p.group(4, "TG: ", ":"): p.text("Breaking(") p.break_() p.text(")") class BreakingRepr: def __repr__(self): return "Breaking(\n)" class BreakingReprParent: def _repr_pretty_(self, p, cycle): with p.group(4, "TG: ", ":"): p.pretty(BreakingRepr()) class BadRepr: def __repr__(self): return 1 / 0 def test_list(): assert pretty.pretty([]) == "[]" assert pretty.pretty([1]) == "[1]" def test_dict(): assert pretty.pretty({}) == "{}" assert pretty.pretty({1: 1}) == "{1: 1}" assert pretty.pretty({1: 1, 0: 0}) == "{1: 1, 0: 0}" # Check that pretty-printing doesn't trigger a BytesWarning under `python -bb` with warnings.catch_warnings(): warnings.simplefilter("ignore", BytesWarning) x = {"": 0, b"": 0} assert pretty.pretty(x) == "{'': 0, b'': 0}" def test_tuple(): assert pretty.pretty(()) == "()" assert pretty.pretty((1,)) == "(1,)" assert pretty.pretty((1, 2)) == "(1, 2)" class ReprDict(dict): def __repr__(self): return "hi" def test_dict_with_custom_repr(): assert pretty.pretty(ReprDict()) == "hi" class ReprList(list): def __repr__(self): return "bye" class ReprSet(set): def __repr__(self): return "cat" def test_set_with_custom_repr(): assert pretty.pretty(ReprSet()) == "cat" def test_list_with_custom_repr(): assert pretty.pretty(ReprList()) == "bye" def test_indentation(): """Test correct indentation in groups.""" count = 40 gotoutput = pretty.pretty(MyList(range(count))) expectedoutput = "MyList(\n" + ",\n".join(f" {i}" for i in range(count)) + ")" assert gotoutput == expectedoutput def test_dispatch(): """Test correct dispatching: The _repr_pretty_ method for MyDict must be found before the registered printer for dict.""" gotoutput = pretty.pretty(MyDict()) expectedoutput = "MyDict(...)" assert gotoutput == expectedoutput def test_callability_checking(): """Test that the _repr_pretty_ method is tested for callability and skipped if not.""" gotoutput = pretty.pretty(Dummy2()) expectedoutput = "Dummy2()" assert gotoutput == expectedoutput def test_sets(): """Test that set and frozenset use Python 3 formatting.""" objects = [ set(), frozenset(), {1}, frozenset([1]), {1, 2}, frozenset([1, 2]), {-1, -2, -3}, ] expected = [ "set()", "frozenset()", "{1}", "frozenset({1})", "{1, 2}", "frozenset({1, 2})", "{-3, -2, -1}", ] for obj, expected_output in zip(objects, expected, strict=True): got_output = pretty.pretty(obj) assert got_output == expected_output def test_unsortable_set(): xs = {1, 2, 3, "foo", "bar", "baz", object()} p = pretty.pretty(xs) for x in xs: assert pretty.pretty(x) in p def test_unsortable_dict(): xs = {k: 1 for k in [1, 2, 3, "foo", "bar", "baz", object()]} p = pretty.pretty(xs) for x in xs: assert pretty.pretty(x) in p def test_pprint_nomod(): """Test that pprint works for classes with no __module__.""" output = pretty.pretty(NoModule) assert output == "NoModule" def test_pprint_break(): """Test that p.break_ produces expected output.""" output = pretty.pretty(Breaking()) expected = "TG: Breaking(\n ):" assert output == expected def test_pprint_break_repr(): """Test that p.break_ is used in repr.""" output = pretty.pretty(BreakingReprParent()) expected = "TG: Breaking(\n ):" assert output == expected def test_bad_repr(): """Don't catch bad repr errors.""" with pytest.raises(ZeroDivisionError): pretty.pretty(BadRepr()) class BadException(Exception): def __str__(self): return -1 # noqa class ReallyBadRepr: __module__ = 1 @property def __class__(self): raise ValueError("I am horrible") def __repr__(self): raise BadException def test_really_bad_repr(): with pytest.raises(BadException): pretty.pretty(ReallyBadRepr()) class SA: pass class SB(SA): pass try: super(SA).__self__ def test_super_repr(): output = pretty.pretty(super(SA)) assert "SA" in output sb = SB() output = pretty.pretty(super(SA, sb)) assert "SA" in output except AttributeError: def test_super_repr(): pretty.pretty(super(SA)) sb = SB() pretty.pretty(super(SA, sb)) def test_long_list(): lis = list(range(10000)) p = pretty.pretty(lis) last2 = p.rsplit("\n", 2)[-2:] assert last2 == [" 999,", " ...]"] def test_long_set(): s = set(range(10000)) p = pretty.pretty(s) last2 = p.rsplit("\n", 2)[-2:] assert last2 == [" 999,", " ...}"] def test_long_tuple(): tup = tuple(range(10000)) p = pretty.pretty(tup) last2 = p.rsplit("\n", 2)[-2:] assert last2 == [" 999,", " ...)"] def test_long_dict(): d = {n: n for n in range(10000)} p = pretty.pretty(d) last2 = p.rsplit("\n", 2)[-2:] assert last2 == [" 999: 999,", " ...}"] def test_unbound_method(): assert pretty.pretty(MyObj.somemethod) == "somemethod" class MetaClass(type): def __new__(metacls, name): return type.__new__(metacls, name, (object,), {"name": name}) def __repr__(cls): return f"[CUSTOM REPR FOR CLASS {cls.name}]" ClassWithMeta = MetaClass("ClassWithMeta") def test_metaclass_repr(): output = pretty.pretty(ClassWithMeta) assert output == "[CUSTOM REPR FOR CLASS ClassWithMeta]" def test_unicode_repr(): u = "üniçodé" class C: def __repr__(self): return u c = C() p = pretty.pretty(c) assert p == u p = pretty.pretty([c]) assert p == f"[{u}]" def test_basic_class(): def type_pprint_wrapper(obj, p, cycle): if obj is MyObj: type_pprint_wrapper.called = True return pretty._type_pprint(obj, p, cycle) type_pprint_wrapper.called = False printer = pretty.RepresentationPrinter() printer.type_pprinters[type] = type_pprint_wrapper printer.pretty(MyObj) output = printer.getvalue() assert output == f"{__name__}.MyObj" assert type_pprint_wrapper.called def test_collections_defaultdict(): # Create defaultdicts with cycles a = defaultdict() a.default_factory = a b = defaultdict(list) b["key"] = b # Dictionary order cannot be relied on, test against single keys. cases = [ (defaultdict(list), "defaultdict(list, {})"), ( defaultdict(list, {"key": "-" * 50}), "defaultdict(list,\n" " {'key': '-----------------------------------------" "---------'})", ), (a, "defaultdict(defaultdict(...), {})"), (b, "defaultdict(list, {'key': defaultdict(...)})"), ] for obj, expected in cases: assert pretty.pretty(obj) == expected @pytest.mark.skipif(PYPY, reason="slightly different on PyPy3") def test_collections_ordereddict(): # Create OrderedDict with cycle a = OrderedDict() a["key"] = a cases = [ (OrderedDict(), "OrderedDict()"), ( OrderedDict((i, i) for i in range(1000, 1010)), "OrderedDict([(1000, 1000),\n" " (1001, 1001),\n" " (1002, 1002),\n" " (1003, 1003),\n" " (1004, 1004),\n" " (1005, 1005),\n" " (1006, 1006),\n" " (1007, 1007),\n" " (1008, 1008),\n" " (1009, 1009)])", ), (a, "OrderedDict([('key', OrderedDict(...))])"), ] for obj, expected in cases: assert pretty.pretty(obj) == expected def test_collections_deque(): # Create deque with cycle a = deque() a.append(a) cases = [ (deque(), "deque([])"), (deque([1, 2, 3]), "deque([1, 2, 3])"), ( deque(i for i in range(1000, 1020)), "deque([1000,\n" " 1001,\n" " 1002,\n" " 1003,\n" " 1004,\n" " 1005,\n" " 1006,\n" " 1007,\n" " 1008,\n" " 1009,\n" " 1010,\n" " 1011,\n" " 1012,\n" " 1013,\n" " 1014,\n" " 1015,\n" " 1016,\n" " 1017,\n" " 1018,\n" " 1019])", ), (a, "deque([deque(...)])"), ] for obj, expected in cases: assert pretty.pretty(obj) == expected def test_collections_counter(): class MyCounter(Counter): pass cases = [ (Counter(), "Counter()"), (Counter(a=1), "Counter({'a': 1})"), (MyCounter(a=1), "MyCounter({'a': 1})"), ] for obj, expected in cases: assert pretty.pretty(obj) == expected def test_cyclic_list(): x = [] x.append(x) assert pretty.pretty(x) == "[[...]]" def test_cyclic_dequeue(): x = deque() x.append(x) assert pretty.pretty(x) == "deque([deque(...)])" class HashItAnyway: def __init__(self, value): self.value = value def __hash__(self): return 0 def __eq__(self, other): return isinstance(other, HashItAnyway) and self.value == other.value def __ne__(self, other): return not self.__eq__(other) def _repr_pretty_(self, pretty, cycle): pretty.pretty(self.value) def test_cyclic_counter(): c = Counter() k = HashItAnyway(c) c[k] = 1 assert pretty.pretty(c) == "Counter({Counter(...): 1})" def test_cyclic_dict(): x = {} k = HashItAnyway(x) x[k] = x assert pretty.pretty(x) == "{{...}: {...}}" def test_cyclic_set(): x = set() x.add(HashItAnyway(x)) assert pretty.pretty(x) == "{{...}}" class BigList(list): def _repr_pretty_(self, printer, cycle): if cycle: return "[...]" else: with printer.group(open="[", close="]"), printer.indent(5): for v in self: printer.pretty(v) printer.breakable(",") def test_print_with_indent(): pretty.pretty(BigList([1, 2, 3])) class MyException(Exception): pass def test_exception(): assert pretty.pretty(ValueError("hi")) == "ValueError('hi')" assert pretty.pretty(ValueError("hi", "there")) == "ValueError('hi', 'there')" assert "test_pretty." in pretty.pretty(MyException()) def test_re_evals(): for r in [ re.compile(r"hi"), re.compile(r"b\nc", re.MULTILINE), re.compile(rb"hi", 0), re.compile("foo", re.MULTILINE | re.UNICODE), ]: r2 = eval(pretty.pretty(r), globals()) assert r.pattern == r2.pattern assert r.flags == r2.flags def test_print_builtin_function(): assert pretty.pretty(abs) == "abs" def test_pretty_function(): assert pretty.pretty(test_pretty_function) == "test_pretty_function" def test_breakable_at_group_boundary(): assert "\n" in pretty.pretty([[], "0" * 80]) @pytest.mark.parametrize( "obj, rep", [ (float("nan"), "nan"), (-float("nan"), "-nan"), (SIGNALING_NAN, "struct.unpack('d', struct.pack('Q', 0x7ff8000000000001))[0]"), (-SIGNALING_NAN, "struct.unpack('d', struct.pack('Q', 0xfff8000000000001))[0]"), ], ) def test_nan_reprs(obj, rep): assert pretty.pretty(obj) == rep assert float_to_lex(obj) == float_to_lex( eval(rep, {"struct": struct, "nan": float("nan")}) ) def _repr_call(*args, **kwargs): p = pretty.RepresentationPrinter() p.repr_call(*args, **kwargs) return p.getvalue() @pytest.mark.parametrize("func_name", ["f", "lambda: ...", "lambda *args: ..."]) def test_repr_call(func_name): fn = f"({func_name})" if func_name.startswith(("lambda:", "lambda ")) else func_name aas = "a" * 100 assert _repr_call(func_name, (1, 2), {}) == f"{fn}(1, 2)" assert _repr_call(func_name, (aas,), {}) == f"{fn}(\n {aas!r},\n)" assert _repr_call(func_name, (), {"a": 1, "b": 2}) == f"{fn}(a=1, b=2)" assert _repr_call(func_name, (), {"x": aas}) == f"{fn}(\n x={aas!r},\n)" class AnEnum(Enum): SOME_MEMBER = 1 class Options(Flag): A = 1 B = 2 C = 4 class EvilReprOptions(Flag): A = 1 B = 2 def __repr__(self): return "can't parse this nonsense" class LyingReprOptions(Flag): A = 1 B = 2 def __repr__(self): return "LyingReprOptions.A|B|C" @pytest.mark.parametrize( "rep", [ "AnEnum.SOME_MEMBER", "Options.A", "Options.A | Options.B", "Options.A | Options.B | Options.C", "Options(0)", "EvilReprOptions.A", "LyingReprOptions.A", "EvilReprOptions.A | EvilReprOptions.B", "LyingReprOptions.A | LyingReprOptions.B", ], ) def test_pretty_prints_enums_as_code(rep): assert pretty.pretty(eval(rep)) == rep class Obj: def _repr_pretty_(self, p, cycle): """Exercise the IPython callback interface.""" assert not cycle with p.indent(2): p.text("abc,") p.breakable(" ") p.break_() p.begin_group(8, "<") p.end_group(8, ">") def test_supports_ipython_callback(): assert pretty.pretty(Obj()) == "abc, \n <>" def test_pretty_partial_with_cycle(): ls = [] p = partial(bool, ls) assert pretty.pretty(p) == "functools.partial(bool, [])" ls.append(p) assert pretty.pretty(p) == "functools.partial(bool, [functools.partial(bool, ...)])" class InvalidSyntaxRepr: def __init__(self, val=None) -> None: self.val = val def __repr__(self): return "invalid syntax" class ValidSyntaxRepr: def __init__(self, val=None) -> None: self.val = val def __repr__(self): return "ValidSyntaxRepr(...)" @given(st.data()) def test_pprint_with_call_or_repr_as_call(data): # mapped pprint repr only triggers for failing examples - which makes an # end to end test given hypothesis difficult. fake our way around it. current_build_context().is_final = True x = data.draw(st.none().map(InvalidSyntaxRepr)) p = pretty.RepresentationPrinter(context=current_build_context()) p.pretty(x) assert p.getvalue() == "InvalidSyntaxRepr(None)" @given(st.just(InvalidSyntaxRepr()).map(ValidSyntaxRepr)) def test_pprint_with_call_or_repr_as_repr(x): p = pretty.RepresentationPrinter(context=current_build_context()) p.pretty(x) assert p.getvalue() == "ValidSyntaxRepr(...)" @given(st.data()) def test_pprint_map_with_cycle(data): current_build_context().is_final = True x = data.draw(st.just(ValidSyntaxRepr()).map(lambda x: x)) p = pretty.RepresentationPrinter(context=current_build_context()) p.pretty(x) assert p.getvalue() == "ValidSyntaxRepr(...)" def test_pprint_large_integers(): p = pretty.RepresentationPrinter() p.pretty(1234567890) assert p.getvalue() == "1_234_567_890" def test_pprint_extremely_large_integers(): x = 10**5000 # repr fails with ddos error p = pretty.RepresentationPrinter() p.pretty(x) got = p.getvalue() assert got == f"{x:#_x}" # hexadecimal with underscores assert eval(got) == x class ReprDetector: def _repr_pretty_(self, p, cycle): """Exercise the IPython callback interface.""" p.text("GOOD") def __repr__(self): return "BAD" @dataclass class SomeDataClass: x: object def test_pretty_prints_data_classes(): assert pretty.pretty(SomeDataClass(ReprDetector())) == "SomeDataClass(x=GOOD)" def test_handles_cycles_in_dataclass(): x = SomeDataClass(x=1) x.x = x assert pretty.pretty(x) == "SomeDataClass(x=SomeDataClass(...))" @dataclass class DataClassWithNoInitField: x: int y: int = field(init=False) def test_does_not_include_no_init_fields_in_dataclass_printing(): record = DataClassWithNoInitField(x=1) assert pretty.pretty(record) == "DataClassWithNoInitField(x=1)" record.y = 1 assert pretty.pretty(record) == "DataClassWithNoInitField(x=1)" class Namespace: @dataclass class DC: x: int class E(Enum): A = 1 NAMESPACED_VALUES = [ Namespace.DC(x=1), Namespace.E.A, ] @pytest.mark.parametrize("obj", NAMESPACED_VALUES, ids=map(repr, NAMESPACED_VALUES)) def test_includes_namespace_classes_in_pretty(obj): assert pretty.pretty(obj).startswith("Namespace.") class Banana: def _repr_pretty_(self, p, cycle): p.text("I am a banana") @dataclass class InheritsPretty(Banana): x: int y: int def test_uses_defined_pretty_printing_method(): assert pretty.pretty(InheritsPretty(x=1, y=2)) == pretty.pretty(Banana()) def test_prefers_singleton_printing_to_repr_pretty(): out = io.StringIO() printer = pretty.RepresentationPrinter(out) banana = Banana() printer.singleton_pprinters[id(banana)] = lambda obj, p, cycle: p.text( "Actually a fish" ) printer.pretty(banana) assert "Actually a fish" in out.getvalue() def test_tuple_pprinter_cycle(): # Test that _tuple_pprinter handles cycles correctly from hypothesis.vendor.pretty import _tuple_pprinter t = (1, 2, 3) arg_labels = {"arg[0]": (0, 1), "arg[1]": (1, 2), "arg[2]": (2, 3)} pprinter = _tuple_pprinter(arg_labels) out = io.StringIO() p = pretty.RepresentationPrinter(out) # Simulate a cycle by adding the tuple's id to the stack p.stack.append(id(t)) pprinter(t, p, cycle=True) p.flush() assert out.getvalue() == "(...)" def test_fixeddict_pprinter_cycle(): # Test that _fixeddict_pprinter handles cycles correctly from hypothesis.vendor.pretty import _fixeddict_pprinter d = {"a": 1, "b": 2} mapping = {"a": None, "b": None} # dummy mapping for key ordering arg_labels = {"a": (0, 1), "b": (1, 2)} pprinter = _fixeddict_pprinter(arg_labels, mapping) out = io.StringIO() p = pretty.RepresentationPrinter(out) # Simulate a cycle by adding the dict's id to the stack p.stack.append(id(d)) pprinter(d, p, cycle=True) p.flush() assert out.getvalue() == "{...}" def test_get_slice_comment_skips_already_commented(): # Test that _get_slice_comment returns None for already-commented slices from hypothesis.vendor.pretty import _get_slice_comment out = io.StringIO() p = pretty.RepresentationPrinter(out) p.slice_comments = {(0, 5): "or any other generated value"} # Mark the slice as already commented p._commented_slices.add((0, 5)) arg_labels = {"arg[0]": (0, 5)} # Should return None because slice is already in _commented_slices result = _get_slice_comment(p, arg_labels, "arg[0]") assert result is None ================================================ FILE: hypothesis-python/tests/cover/test_provisional_strategies.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import string import pytest from hypothesis import given, settings from hypothesis.errors import InvalidArgument from hypothesis.provisional import ( FRAGMENT_SAFE_CHARACTERS, _url_fragments_strategy, domains, urls, ) from tests.common.debug import check_can_generate_examples, find_any @given(urls()) def test_is_URL(url): allowed_chars = set(string.ascii_letters + string.digits + "$-_.+!*'(),~%/") url_schemeless = url.split("://", 1)[1] components = url_schemeless.split("#", 1) domain_path = components[0] path = domain_path.split("/", 1)[1] if "/" in domain_path else "" assert all(c in allowed_chars for c in path) assert all( re.match("^[0-9A-Fa-f]{2}", after_perc) for after_perc in path.split("%")[1:] ) fragment = components[1] if "#" in url_schemeless else "" fragment_allowed_chars = allowed_chars | {"?"} assert all(c in fragment_allowed_chars for c in fragment) assert all( re.match("^[0-9A-Fa-f]{2}", after_perc) for after_perc in fragment.split("%")[1:] ) @pytest.mark.parametrize("max_length", [-1, 0, 3, 4.0, 256]) @pytest.mark.parametrize("max_element_length", [-1, 0, 4.0, 64, 128]) def test_invalid_domain_arguments(max_length, max_element_length): with pytest.raises(InvalidArgument): check_can_generate_examples( domains(max_length=max_length, max_element_length=max_element_length) ) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~300s each; the `sampled_from(get_top_level_domains())` decision is realized via iterative comparisons https://github.com/pschanely/CrossHair/issues/332", ) @pytest.mark.parametrize("max_length", [None, 4, 8, 255]) @pytest.mark.parametrize("max_element_length", [None, 1, 2, 4, 8, 63]) def test_valid_domains_arguments(max_length, max_element_length): check_can_generate_examples( domains(max_length=max_length, max_element_length=max_element_length) ) @pytest.mark.parametrize("strategy", [domains(), urls()]) def test_find_any_non_empty(strategy): find_any(strategy, lambda s: len(s) > 0) @given(_url_fragments_strategy) # There's a lambda in the implementation that only gets run if we generate at # least one percent-escape sequence, so we derandomize to ensure that coverage # isn't flaky. @settings(derandomize=True) def test_url_fragments_contain_legal_chars(fragment): assert fragment.startswith("#") # Strip all legal escape sequences. Any remaining % characters were not # part of a legal escape sequence. without_escapes = re.sub(r"(?ai)%[0-9a-f][0-9a-f]", "", fragment[1:]) assert set(without_escapes).issubset(FRAGMENT_SAFE_CHARACTERS) ================================================ FILE: hypothesis-python/tests/cover/test_random_module.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import gc import random import sys import pytest from hypothesis import ( Phase, core, find, given, register_random, settings, strategies as st, ) from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.internal import entropy from hypothesis.internal.compat import GRAALPY, PYPY from hypothesis.internal.entropy import deterministic_PRNG from tests.common.utils import skipif_threading, xfail_if_gil_disabled def gc_collect(): # CPython uses reference counting, so objects (without circular refs) # are collected immediately on `del`, breaking weak references. # Python implementations with other garbage collection strategies may # or may not, so we use this function in tests before counting the # surviving references to ensure that they're deterministic. if PYPY or GRAALPY: gc.collect() def test_can_seed_random(): @settings(phases=(Phase.generate, Phase.shrink)) @given(st.random_module()) def test(r): raise AssertionError with pytest.raises(AssertionError) as err: test() assert "RandomSeeder(0)" in "\n".join(err.value.__notes__) @given(st.random_module(), st.random_module()) def test_seed_random_twice(r, r2): assert repr(r) == repr(r2) # ideally we would actually raise the global random warning here, but random_module # calls seed_all and restore_all inside the deprecate_random_in_strategy context # manager, which never sees the global random interference. # # But it can sometimes see it under multithreading depending on timing. Until # we fix this to also warn in this case, just skip on threading. @skipif_threading @given(st.random_module()) def test_does_not_fail_health_check_if_randomness_is_used(r): random.getrandbits(128) def test_cannot_register_non_Random(): with pytest.raises(InvalidArgument): register_random("not a Random instance") @skipif_threading def test_registering_a_Random_is_idempotent(): gc_collect() n_registered = len(entropy.RANDOMS_TO_MANAGE) # on 3.14+, python introduced the LOAD_FAST_BORROW opcode, which does # not increment the refcount. Passing a bare r to register_random here on 3.14+ # would use LOAD_FAST_BORROW and entropy.py would see a non-increasing refcount # and hard-error. On 3.13 and earlier, this is a warning instead. # # For compatibility with both versions, this test forces a refcount increment # with a redundant container. container = [random.Random()] r = container[0] register_random(r) register_random(r) assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1 del container del r gc_collect() assert len(entropy.RANDOMS_TO_MANAGE) == n_registered def test_manages_registered_Random_instance(): r = random.Random() register_random(r) state = r.getstate() result = [] @given(st.integers()) def inner(x): v = r.random() if result: assert v == result[0] else: result.append(v) inner() assert state == r.getstate() def test_registered_Random_is_seeded_by_random_module_strategy(): r = random.Random() register_random(r) state = r.getstate() results = set() count = 0 @given(st.integers()) def inner(x): nonlocal count results.add(r.random()) count += 1 inner() assert count > len(results) * 0.9, "too few unique random numbers" assert state == r.getstate() @given(st.random_module()) @skipif_threading # writing to global random state def test_will_actually_use_the_random_seed(rnd): a = random.randint(0, 100) b = random.randint(0, 100) random.seed(rnd.seed) assert a == random.randint(0, 100) assert b == random.randint(0, 100) def test_given_does_not_pollute_state(): with deterministic_PRNG(): @given(st.random_module()) def test(r): pass test() state_a = random.getstate() state_a2 = core.threadlocal._hypothesis_global_random.getstate() test() state_b = random.getstate() state_b2 = core.threadlocal._hypothesis_global_random.getstate() assert state_a == state_b assert state_a2 != state_b2 @skipif_threading # modifying global random state def test_find_does_not_pollute_state(): with deterministic_PRNG(): find(st.random_module(), lambda r: True) state_a = random.getstate() state_a2 = core.threadlocal._hypothesis_global_random.getstate() find(st.random_module(), lambda r: True) state_b = random.getstate() state_b2 = core.threadlocal._hypothesis_global_random.getstate() assert state_a == state_b assert state_a2 != state_b2 @skipif_threading # we assume we're the only writer to entropy.RANDOMS_TO_MANAGE def test_evil_prng_registration_nonsense(): # my guess is that other tests may register randoms that are then marked for # deletion (but not actually gc'd yet). Therefore, depending on the order tests # are run, RANDOMS_TO_MANAGE may start with more entries than after a gc. To # force a clean slate for this test, unconditionally gc. gc.collect() # The first test to call deterministic_PRNG registers a new random instance. # If that's this test, it will throw off our n_registered count in the middle. # start with a no-op to ensure this registration has occurred. with deterministic_PRNG(): pass n_registered = len(entropy.RANDOMS_TO_MANAGE) # put inside a list to increment ref count and avoid our warning/error about no # referrers c1, c2, c3 = [random.Random(1)], [random.Random(2)], [random.Random(3)] r1, r2, r3 = c1[0], c2[0], c3[0] s2 = r2.getstate() # We're going to be totally evil here: register two randoms, then # drop one and add another, and finally check that we reset only # the states that we collected before we started register_random(r1) k = max(entropy.RANDOMS_TO_MANAGE) # get a handle to check if r1 still exists register_random(r2) assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 2 with deterministic_PRNG(): del r1 del c1 gc_collect() assert k not in entropy.RANDOMS_TO_MANAGE, "r1 has been garbage-collected" assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1 r2.seed(4) register_random(r3) r3.seed(4) s4 = r3.getstate() # Implicit check, no exception was raised in __exit__ assert r2.getstate() == s2, "reset previously registered random state" assert r3.getstate() == s4, "retained state when registered within the context" @pytest.mark.skipif( PYPY, reason="We can't guard against bad no-reference patterns in pypy." ) def test_passing_unreferenced_instance_raises(): with pytest.raises(ReferenceError): register_random(random.Random(0)) @xfail_if_gil_disabled @pytest.mark.skipif( PYPY, reason="We can't guard against bad no-reference patterns in pypy." ) def test_passing_unreferenced_instance_within_function_scope_raises(): def f(): register_random(random.Random(0)) with pytest.raises(ReferenceError): f() # we have two error paths for register_random: one which warns and one which # errors. We use an alias to bump the refcount while not adding a gc referrer, # which covers the warning path. def f(): r = random.Random(0) _r2 = r register_random(r) with pytest.warns( HypothesisWarning, match="It looks like `register_random` was passed an object that could be" " garbage collected", ): f() @xfail_if_gil_disabled @pytest.mark.skipif( PYPY, reason="We can't guard against bad no-reference patterns in pypy." ) @pytest.mark.skipif( sys.version_info[:2] < (3, 14), reason="warns instead of raises on 3.13 or earlier due to gc changes", ) def test_passing_referenced_instance_within_function_scope_raises(): def f(): r = random.Random(0) register_random(r) with pytest.raises( ReferenceError, match=r"`register_random` was passed .* which will be garbage collected", ): f() @pytest.mark.skipif( PYPY, reason="We can't guard against bad no-reference patterns in pypy." ) @skipif_threading # we assume we're the only writer to entropy.RANDOMS_TO_MANAGE def test_register_random_within_nested_function_scope(): n_registered = len(entropy.RANDOMS_TO_MANAGE) def f(): # put inside a list to increment ref count and avoid our warning/error about no # referrers container = [random.Random()] r = container[0] register_random(r) assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1 f() gc_collect() assert len(entropy.RANDOMS_TO_MANAGE) == n_registered ================================================ FILE: hypothesis-python/tests/cover/test_randoms.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import inspect import math from copy import copy import pytest from hypothesis import HealthCheck, assume, given, settings, strategies as st from hypothesis.internal.compat import ExceptionGroup from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.strategies._internal.random import ( RANDOM_METHODS, ArtificialRandom, HypothesisRandom, TrueRandom, convert_kwargs, normalize_zero, ) from tests.common.debug import assert_all_examples, find_any from tests.common.utils import Why, xfail_on_crosshair def test_implements_all_random_methods(): for name in dir(HypothesisRandom): if not name.startswith("_") or name == "_randbelow": f = getattr(HypothesisRandom, name) if inspect.isfunction(f): assert f.__module__ == "hypothesis.strategies._internal.random", name any_random = st.randoms(use_true_random=False) | st.randoms(use_true_random=True) beta_param = st.floats(0.01, 1000) seq_param = st.lists(st.integers(), min_size=1) METHOD_STRATEGIES = {} def define_method_strategy(name, **kwargs): METHOD_STRATEGIES[name] = kwargs define_method_strategy("betavariate", alpha=beta_param, beta=beta_param) define_method_strategy("binomialvariate", n=st.integers(min_value=1), p=st.floats(0, 1)) define_method_strategy("gammavariate", alpha=beta_param, beta=beta_param) define_method_strategy("weibullvariate", alpha=beta_param, beta=beta_param) define_method_strategy("choice", seq=seq_param) define_method_strategy("choices", population=seq_param, k=st.integers(1, 100)) define_method_strategy("expovariate", lambd=beta_param) define_method_strategy("_randbelow", n=st.integers(1, 2**64)) define_method_strategy("random") define_method_strategy("getrandbits", n=st.integers(1, 128)) define_method_strategy("gauss", mu=st.floats(-1000, 1000), sigma=beta_param) define_method_strategy("normalvariate", mu=st.floats(-1000, 1000), sigma=beta_param) # the standard library lognormalvariate is weirdly bad at handling large floats define_method_strategy( "lognormvariate", mu=st.floats(0.1, 10), sigma=st.floats(0.1, 10) ) define_method_strategy( "vonmisesvariate", mu=st.floats(0, math.pi * 2), kappa=beta_param ) # Small alpha may raise ZeroDivisionError, see https://bugs.python.org/issue41421 define_method_strategy("paretovariate", alpha=st.floats(min_value=1.0)) define_method_strategy("shuffle", x=st.lists(st.integers())) define_method_strategy("randbytes", n=st.integers(0, 100)) INT64 = st.integers(-(2**63), 2**63 - 1) @st.composite def any_call_of_method(draw, method): if method == "sample": population = draw(seq_param) k = draw(st.integers(0, len(population))) kwargs = {"population": population, "k": k} elif method == "randint": a = draw(INT64) b = draw(INT64) a, b = sorted((a, b)) kwargs = {"a": a, "b": b} elif method == "randrange": a = draw(INT64) b = draw(INT64) assume(a != b) a, b = sorted((a, b)) kwargs = {"start": a, "stop": b, "step": draw(st.integers(1, 3))} elif method == "triangular": a = normalize_zero(draw(st.floats(allow_infinity=False, allow_nan=False))) b = normalize_zero(draw(st.floats(allow_infinity=False, allow_nan=False))) a, b = sorted((a, b)) if draw(st.booleans()): draw(st.floats(a, b)) kwargs = {"low": a, "high": b, "mode": None} elif method == "uniform": a = normalize_zero(draw(st.floats(allow_infinity=False, allow_nan=False))) b = normalize_zero(draw(st.floats(allow_infinity=False, allow_nan=False))) a, b = sorted((a, b)) kwargs = {"a": a, "b": b} else: kwargs = draw(st.fixed_dictionaries(METHOD_STRATEGIES[method])) args, kwargs = convert_kwargs(method, kwargs) return (args, kwargs) @st.composite def any_call(draw): method = draw(st.sampled_from(RANDOM_METHODS)) return (method, *draw(any_call_of_method(method))) @pytest.mark.parametrize("method", RANDOM_METHODS) @given(any_random, st.data()) def test_call_all_methods(method, rnd, data): args, kwargs = data.draw(any_call_of_method(method)) getattr(rnd, method)(*args, **kwargs) @given(any_random, st.integers(1, 100)) def test_rand_below(rnd, n): assert rnd._randbelow(n) < n @given(any_random, beta_param, beta_param) def test_beta_in_range(rnd, a, b): assert 0 <= rnd.betavariate(a, b) <= 1 def test_multiple_randoms_are_unrelated(): @given(st.randoms(use_true_random=False), st.randoms(use_true_random=False)) def test(r1, r2): assert r1.random() == r2.random() with pytest.raises(AssertionError): test() @pytest.mark.parametrize("use_true_random", [False, True]) @given(data=st.data()) def test_randoms_can_be_synced(use_true_random, data): r1 = data.draw(st.randoms(use_true_random=use_true_random)) r2 = data.draw(st.randoms(use_true_random=use_true_random)) r2.setstate(r1.getstate()) assert r1.random() == r2.random() @pytest.mark.parametrize("use_true_random", [False, True]) @given(data=st.data(), method_call=any_call()) def test_seeding_to_same_value_synchronizes(use_true_random, data, method_call): r1 = data.draw(st.randoms(use_true_random=use_true_random)) r2 = data.draw(st.randoms(use_true_random=use_true_random)) method, args, kwargs = method_call r1.seed(0) r2.seed(0) assert getattr(r1, method)(*args, **kwargs) == getattr(r2, method)(*args, **kwargs) @given(any_random, any_call()) def test_copying_synchronizes(r1, method_call): method, args, kwargs = method_call r2 = copy(r1) assert getattr(r1, method)(*args, **kwargs) == getattr(r2, method)(*args, **kwargs) @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) @pytest.mark.parametrize("use_true_random", [True, False]) def test_seeding_to_different_values_does_not_synchronize(use_true_random): @given( st.randoms(use_true_random=use_true_random), st.randoms(use_true_random=use_true_random), ) def test(r1, r2): r1.seed(0) r2.seed(1) assert r1.random() == r2.random() with pytest.raises(AssertionError): test() @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) @pytest.mark.parametrize("use_true_random", [True, False]) def test_unrelated_calls_desynchronizes(use_true_random): @given( st.randoms(use_true_random=use_true_random), st.randoms(use_true_random=use_true_random), ) def test(r1, r2): r1.seed(0) r2.seed(0) r1.randrange(1, 10) r2.getrandbits(128) assert r1.random() == r2.random() with pytest.raises(AssertionError): test() @given(st.randoms(use_true_random=False), st.randoms(use_true_random=False)) def test_state_is_consistent(r1, r2): r2.setstate(r1.getstate()) assert r1.getstate() == r2.getstate() @given(st.randoms()) def test_does_not_use_true_random_by_default(rnd): assert not isinstance(rnd, TrueRandom) @given(st.randoms(use_true_random=False)) def test_handles_singleton_uniforms_correctly(rnd): assert rnd.uniform(1.0, 1.0) == 1.0 assert rnd.uniform(0.0, 0.0) == 0.0 assert rnd.uniform(-0.0, -0.0) == 0.0 assert rnd.uniform(0.0, -0.0) == 0.0 @given(st.randoms(use_true_random=False)) def test_handles_singleton_regions_of_triangular_correctly(rnd): assert rnd.triangular(1.0, 1.0) == 1.0 assert rnd.triangular(0.0, 0.0) == 0.0 assert rnd.triangular(-0.0, -0.0) == 0.0 assert rnd.triangular(0.0, -0.0) == 0.0 @given(st.randoms(use_true_random=False)) def test_triangular_with_mode(rnd): x = rnd.triangular(0.0, 1.0, mode=0.5) assert 0.0 <= x <= 1.0 @pytest.mark.parametrize("use_true_random", [False, True]) def test_outputs_random_calls(use_true_random): @given(st.randoms(use_true_random=use_true_random, note_method_calls=True)) def test(rnd): rnd.uniform(0.1, 0.5) raise AssertionError with pytest.raises(AssertionError) as err: test() assert ".uniform(0.1, 0.5)" in "\n".join(err.value.__notes__) @pytest.mark.skipif( "choices" not in RANDOM_METHODS, reason="choices not supported on this Python version", ) @pytest.mark.parametrize("use_true_random", [False, True]) def test_converts_kwargs_correctly_in_output(use_true_random): @given(st.randoms(use_true_random=use_true_random, note_method_calls=True)) def test(rnd): rnd.choices([1, 2, 3, 4], k=2) raise AssertionError with pytest.raises(AssertionError) as err: test() assert ".choices([1, 2, 3, 4], k=2)" in "\n".join(err.value.__notes__) @given(st.randoms(use_true_random=False)) def test_some_ranges_are_in_range(rnd): assert 0 <= rnd.randrange(10) < 10 assert 11 <= rnd.randrange(11, 20) < 20 assert rnd.randrange(1, 100, 3) in range(1, 100, 3) assert rnd.randrange(100, step=3) in range(0, 100, 3) def test_invalid_range(): @given(st.randoms(use_true_random=False)) def test(rnd): rnd.randrange(1, 1) with pytest.raises(ValueError): test() def test_invalid_sample(): @given(st.randoms(use_true_random=False)) def test(rnd): rnd.sample([1, 2], 3) with pytest.raises(ValueError): test() def test_triangular_modes(): @settings(report_multiple_bugs=True) @given(st.randoms(use_true_random=False)) def test(rnd): x = rnd.triangular(0.0, 1.0, mode=0.5) assert x < 0.5 assert x > 0.5 with pytest.raises(ExceptionGroup): test() @given(st.randoms(use_true_random=False), any_call_of_method("sample")) def test_samples_have_right_length(rnd, sample): (seq, k), _ = sample assert len(rnd.sample(seq, k)) == k @given(st.randoms(use_true_random=False), any_call_of_method("choices")) def test_choices_have_right_length(rnd, choices): args, kwargs = choices seq = args[0] k = kwargs.get("k", 1) assert len(rnd.choices(seq, k=k)) == k @given(any_random, st.integers(0, 100)) def test_randbytes_have_right_length(rnd, n): assert len(rnd.randbytes(n)) == n @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes hours; may get faster after https://github.com/pschanely/CrossHair/issues/332", ) @given(any_random) def test_can_manage_very_long_ranges_with_step(rnd): i = rnd.randrange(0, 2**256, 3) assert i % 3 == 0 assert 0 <= i < 2**256 assert i in range(0, 2**256, 3) @settings(suppress_health_check=[HealthCheck.too_slow]) @given(any_random, st.data()) def test_range_with_arbitrary_step_is_in_range(rnd, data): endpoints = st.integers(-100, 100) step = data.draw(st.integers(1, 3)) start, stop = sorted((data.draw(endpoints), data.draw(endpoints))) assume(start < stop) i = rnd.randrange(start, stop, step) assert i in range(start, stop, step) @given(any_random, st.integers(min_value=1)) def test_range_with_only_stop(rnd, n): assert 0 <= rnd.randrange(n) < n def test_can_find_end_of_range(): find_any( st.randoms(use_true_random=False).map(lambda r: r.randrange(0, 11, 2)), lambda n: n == 10, ) find_any( st.randoms(use_true_random=False).map(lambda r: r.randrange(0, 10, 2)), lambda n: n == 8, ) @given(st.randoms(use_true_random=False)) def test_can_sample_from_whole_range(rnd): xs = list(map(str, range(10))) ys = rnd.sample(xs, len(xs)) assert sorted(ys) == sorted(xs) @given(st.randoms(use_true_random=False)) def test_can_sample_from_large_subset(rnd): xs = list(map(str, range(10))) n = len(xs) // 3 ys = rnd.sample(xs, n) assert set(ys).issubset(set(xs)) assert len(ys) == len(set(ys)) == n @given(st.randoms(use_true_random=False)) def test_can_draw_empty_from_empty_sequence(rnd): assert rnd.sample([], 0) == [] def test_random_includes_zero_excludes_one(): strat = st.randoms(use_true_random=False).map(lambda r: r.random()) assert_all_examples(strat, lambda x: 0 <= x < 1) find_any(strat, lambda x: x == 0) def test_betavariate_includes_zero_and_one(): # https://github.com/HypothesisWorks/hypothesis/issues/4297#issuecomment-2720144709 strat = st.randoms(use_true_random=False).flatmap( lambda r: st.builds( r.betavariate, alpha=st.just(1.0) | beta_param, beta=beta_param ) ) assert_all_examples(strat, lambda x: 0 <= x <= 1) find_any(strat, lambda x: x == 0) find_any(strat, lambda x: x == 1) def test_artificial_random_with_already_initialized_states_for_ids(): # covering test for calling .getstate when data.states_for_ids is not None. data = ConjectureData.for_choices([]) data.states_for_ids = {} r = ArtificialRandom(note_method_calls=False, data=data) r.getstate() ================================================ FILE: hypothesis-python/tests/cover/test_recursive.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, note, settings, strategies as st from hypothesis.errors import HypothesisWarning, InvalidArgument from tests.common.debug import ( assert_all_examples, assert_no_examples, check_can_generate_examples, find_any, minimal, ) from tests.common.utils import checks_deprecated_behaviour @given(st.recursive(st.booleans(), st.lists, max_leaves=10)) def test_respects_leaf_limit(xs): def flatten(x): if isinstance(x, list): return sum(map(flatten, x), []) else: return [x] assert len(flatten(xs)) <= 10 def test_can_find_nested(): x = minimal( st.recursive(st.booleans(), lambda x: st.tuples(x, x)), lambda x: isinstance(x, tuple) and isinstance(x[0], tuple), ) assert x == ((False, False), False) def test_recursive_call_validates_expand_returns_strategies(): with pytest.raises(InvalidArgument): check_can_generate_examples(st.recursive(st.booleans(), lambda x: 1)) def test_recursive_call_validates_base_is_strategy(): x = st.recursive(1, lambda x: st.none()) with pytest.raises(InvalidArgument): check_can_generate_examples(x) def test_can_find_exactly_max_leaves(): strat = st.recursive(st.none(), lambda x: st.tuples(x, x), max_leaves=5) def enough_leaves(t): print(t) count = 0 stack = [t] while stack: s = stack.pop() if s is None: count += 1 else: stack.extend(s) return count >= 5 find_any(strat, enough_leaves) @given(st.recursive(st.none(), lambda x: st.tuples(x, x), max_leaves=1)) def test_can_exclude_branching_with_max_leaves(t): assert t is None @given(st.recursive(st.none(), lambda x: st.one_of(x, x))) def test_issue_1502_regression(s): pass def test_recursive_can_generate_varied_structures(): values = st.recursive(st.none(), st.lists) find_any(values, lambda x: x is None) find_any(values, lambda x: isinstance(x, list)) find_any( values, lambda x: isinstance(x, list) and any(isinstance(y, list) for y in x) ) @checks_deprecated_behaviour def test_recursive_can_generate_varied_structures_without_using_leaves(): values = st.recursive(st.none(), lambda _: st.lists(st.none())) find_any(values, lambda x: x is None) find_any(values, lambda x: isinstance(x, list)) # The bad `extend` function means we can't actually recurse! assert_no_examples( values, lambda x: isinstance(x, list) and any(isinstance(y, list) for y in x) ) @pytest.mark.parametrize( "s", [ st.recursive(None, st.lists), st.recursive(st.none(), lambda x: None), st.recursive(st.none(), st.lists, max_leaves=-1), st.recursive(st.none(), st.lists, max_leaves=0), st.recursive(st.none(), st.lists, max_leaves=1.0), st.recursive(st.none(), st.lists, min_leaves=-1), st.recursive(st.none(), st.lists, min_leaves=0), st.recursive(st.none(), st.lists, min_leaves=1.0), st.recursive(st.none(), st.lists, min_leaves=10, max_leaves=5), st.recursive(st.none(), lambda _: st.lists(st.none()), min_leaves=1), ], ) def test_invalid_args(s): with pytest.raises(InvalidArgument): check_can_generate_examples(s) def _count_leaves(tree): if isinstance(tree, tuple): return sum(_count_leaves(child) for child in tree) return 1 @given(st.data()) @settings(max_examples=5, suppress_health_check=["filter_too_much"]) def test_respects_min_leaves(data): min_leaves = data.draw(st.integers(1, 20)) max_leaves = data.draw(st.integers(min_leaves, 40)) note(f"{min_leaves=}") note(f"{max_leaves=}") s = st.recursive( st.none(), lambda x: st.tuples(x, x), min_leaves=min_leaves, max_leaves=max_leaves, ) assert_all_examples(s, lambda tree: min_leaves <= _count_leaves(tree) <= max_leaves) @given(st.recursive(st.none(), lambda x: st.tuples(x, x), min_leaves=5, max_leaves=5)) @settings(suppress_health_check=["filter_too_much"]) def test_can_set_exact_leaf_count(tree): assert _count_leaves(tree) == 5 def test_identity_extend_warns(): with pytest.warns(HypothesisWarning, match="extend=lambda x: x is a no-op"): st.recursive(st.none(), lambda x: x).validate() ================================================ FILE: hypothesis-python/tests/cover/test_reflection.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys from copy import deepcopy from datetime import time from functools import partial, wraps from inspect import Parameter, Signature, signature from textwrap import dedent from unittest.mock import MagicMock, Mock, NonCallableMagicMock, NonCallableMock import pytest from pytest import raises from hypothesis import assume, example, given, strategies as st from hypothesis.errors import HypothesisWarning from hypothesis.internal import lambda_sources, reflection from hypothesis.internal.reflection import ( convert_keyword_arguments, convert_positional_arguments, define_function_signature, function_digest, get_pretty_function_description, get_signature, is_first_param_referenced_in_function, is_identity_function, is_mock, proxies, repr_call, required_args, source_exec_as_module, ) from hypothesis.strategies._internal.lazy import LazyStrategy def do_conversion_test(f, args, kwargs): result = f(*args, **kwargs) cargs, ckwargs = convert_keyword_arguments(f, args, kwargs) assert result == f(*cargs, **ckwargs) cargs2, ckwargs2 = convert_positional_arguments(f, args, kwargs) assert result == f(*cargs2, **ckwargs2) def test_simple_conversion(): def foo(a, b, c): return (a, b, c) assert convert_keyword_arguments(foo, (1, 2, 3), {}) == ((1, 2, 3), {}) assert convert_keyword_arguments(foo, (), {"a": 3, "b": 2, "c": 1}) == ( (3, 2, 1), {}, ) do_conversion_test(foo, (1, 0), {"c": 2}) do_conversion_test(foo, (1,), {"c": 2, "b": "foo"}) def test_leaves_unknown_kwargs_in_dict(): def bar(x, **kwargs): pass assert convert_keyword_arguments(bar, (1,), {"foo": "hi"}) == ((1,), {"foo": "hi"}) assert convert_keyword_arguments(bar, (), {"x": 1, "foo": "hi"}) == ( (1,), {"foo": "hi"}, ) do_conversion_test(bar, (1,), {}) do_conversion_test(bar, (), {"x": 1, "y": 1}) def test_errors_on_bad_kwargs(): def bar(): pass with raises(TypeError): convert_keyword_arguments(bar, (), {"foo": 1}) def test_passes_varargs_correctly(): def foo(*args): pass assert convert_keyword_arguments(foo, (1, 2, 3), {}) == ((1, 2, 3), {}) do_conversion_test(foo, (1, 2, 3), {}) def test_errors_if_keyword_precedes_positional(): def foo(x, y): pass with raises(TypeError): convert_keyword_arguments(foo, (1,), {"x": 2}) def test_errors_if_not_enough_args(): def foo(a, b, c, d=1): pass with raises(TypeError): convert_keyword_arguments(foo, (1, 2), {"d": 4}) def test_errors_on_extra_kwargs(): def foo(a): pass with raises(TypeError, match="keyword"): convert_keyword_arguments(foo, (1,), {"b": 1}) with raises(TypeError, match="keyword"): convert_keyword_arguments(foo, (1,), {"b": 1, "c": 2}) def test_positional_errors_if_too_many_args(): def foo(a): pass with raises(TypeError, match="too many positional arguments"): convert_positional_arguments(foo, (1, 2), {}) def test_positional_errors_if_too_few_args(): def foo(a, b, c): pass with raises(TypeError): convert_positional_arguments(foo, (1, 2), {}) def test_positional_does_not_error_if_extra_args_are_kwargs(): def foo(a, b, c): pass convert_positional_arguments(foo, (1, 2), {"c": 3}) def test_positional_errors_if_given_bad_kwargs(): def foo(a): pass with raises(TypeError, match="missing a required argument: 'a'"): convert_positional_arguments(foo, (), {"b": 1}) def test_positional_errors_if_given_duplicate_kwargs(): def foo(a): pass with raises(TypeError, match="multiple values"): convert_positional_arguments(foo, (2,), {"a": 1}) def test_names_of_functions_are_pretty(): assert ( get_pretty_function_description(test_names_of_functions_are_pretty) == "test_names_of_functions_are_pretty" ) class Foo: @classmethod def bar(cls): pass def baz(cls): pass def __repr__(self): return "SoNotFoo()" def test_class_names_are_not_included_in_class_method_prettiness(): assert get_pretty_function_description(Foo.bar) == "bar" def test_repr_is_included_in_bound_method_prettiness(): assert get_pretty_function_description(Foo().baz) == "SoNotFoo().baz" def test_class_is_not_included_in_unbound_method(): assert get_pretty_function_description(Foo.baz) == "baz" def test_does_not_error_on_confused_sources(): def ed(f, *args): return f x = ed( lambda x, y: (x * y).conjugate() == x.conjugate() * y.conjugate(), complex, complex, ) get_pretty_function_description(x) def test_digests_are_reasonably_unique(): assert function_digest(test_simple_conversion) != function_digest( test_does_not_error_on_confused_sources ) def test_digest_returns_the_same_value_for_two_calls(): assert function_digest(test_simple_conversion) == function_digest( test_simple_conversion ) def test_can_digest_a_built_in_function(): import math assert function_digest(math.isnan) != function_digest(range) def test_can_digest_a_unicode_lambda(): function_digest(lambda x: "☃" in str(x)) def test_can_digest_a_function_with_no_name(): def foo(x, y): pass function_digest(partial(foo, 1)) def test_arg_string_is_in_order(): def foo(c, a, b, f, a1): pass assert repr_call(foo, (1, 2, 3, 4, 5), {}) == "foo(c=1, a=2, b=3, f=4, a1=5)" assert ( repr_call(foo, (1, 2), {"b": 3, "f": 4, "a1": 5}) == "foo(c=1, a=2, b=3, f=4, a1=5)" ) def test_varkwargs_are_sorted_and_after_real_kwargs(): def foo(d, e, f, **kwargs): pass assert ( repr_call(foo, (), {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6}) == "foo(d=4, e=5, f=6, a=1, b=2, c=3)" ) def test_varargs_come_without_equals(): def foo(a, *args): pass assert repr_call(foo, (1, 2, 3, 4), {}) == "foo(2, 3, 4, a=1)" def test_can_mix_varargs_and_varkwargs(): def foo(*args, **kwargs): pass assert repr_call(foo, (1, 2, 3), {"c": 7}) == "foo(1, 2, 3, c=7)" def test_arg_string_does_not_include_unprovided_defaults(): def foo(a, b, c=9, d=10): pass assert repr_call(foo, (1,), {"b": 1, "d": 11}) == "foo(a=1, b=1, d=11)" def universal_acceptor(*args, **kwargs): return args, kwargs def has_one_arg(hello): pass def has_two_args(hello, world): pass def has_a_default(x, y, z=1): pass def has_varargs(*args): pass def has_kwargs(**kwargs): pass @pytest.mark.parametrize("f", [has_one_arg, has_two_args, has_varargs, has_kwargs]) def test_copying_preserves_signature(f): af = get_signature(f) t = define_function_signature("foo", "docstring", af)(universal_acceptor) at = get_signature(t) assert af == at def test_name_does_not_clash_with_function_names(): def f(): pass @define_function_signature("f", "A docstring for f", signature(f)) def g(): pass g() def test_copying_sets_name(): f = define_function_signature( "hello_world", "A docstring for hello_world", signature(has_two_args) )(universal_acceptor) assert f.__name__ == "hello_world" def test_copying_sets_docstring(): f = define_function_signature( "foo", "A docstring for foo", signature(has_two_args) )(universal_acceptor) assert f.__doc__ == "A docstring for foo" def test_uses_defaults(): f = define_function_signature( "foo", "A docstring for foo", signature(has_a_default) )(universal_acceptor) assert f(3, 2) == ((3, 2, 1), {}) def test_uses_varargs(): f = define_function_signature("foo", "A docstring for foo", signature(has_varargs))( universal_acceptor ) assert f(1, 2) == ((1, 2), {}) DEFINE_FOO_FUNCTION = """ def foo(x): return x """ def test_exec_as_module_execs(): m = source_exec_as_module(DEFINE_FOO_FUNCTION) assert m.foo(1) == 1 def test_exec_as_module_caches(): assert source_exec_as_module(DEFINE_FOO_FUNCTION) is source_exec_as_module( DEFINE_FOO_FUNCTION ) def test_exec_leaves_sys_path_unchanged(): old_path = deepcopy(sys.path) source_exec_as_module("hello_world = 42") assert sys.path == old_path def test_define_function_signature_works_with_conflicts(): def accepts_everything(*args, **kwargs): pass define_function_signature( "hello", "A docstring for hello", Signature(parameters=[Parameter("f", Parameter.POSITIONAL_OR_KEYWORD)]), )(accepts_everything)(1) define_function_signature( "hello", "A docstring for hello", Signature(parameters=[Parameter("f", Parameter.VAR_POSITIONAL)]), )(accepts_everything)(1) define_function_signature( "hello", "A docstring for hello", Signature(parameters=[Parameter("f", Parameter.VAR_KEYWORD)]), )(accepts_everything)() define_function_signature( "hello", "A docstring for hello", Signature( parameters=[ Parameter("f", Parameter.POSITIONAL_OR_KEYWORD), Parameter("f_3", Parameter.POSITIONAL_OR_KEYWORD), Parameter("f_1", Parameter.VAR_POSITIONAL), Parameter("f_2", Parameter.VAR_KEYWORD), ] ), )(accepts_everything)(1, 2) def test_define_function_signature_validates_function_name(): define_function_signature("hello_world", None, Signature()) with raises(ValueError): define_function_signature("hello world", None, Signature()) class Container: def funcy(self): pass def test_can_proxy_functions_with_mixed_args_and_varargs(): def foo(a, *args): return (a, args) @proxies(foo) def bar(*args, **kwargs): return foo(*args, **kwargs) assert bar(1, 2) == (1, (2,)) def test_can_delegate_to_a_function_with_no_positional_args(): def foo(a, b): return (a, b) @proxies(foo) def bar(**kwargs): return foo(**kwargs) assert bar(2, 1) == (2, 1) @pytest.mark.parametrize( "func,args,expected", [ (lambda: None, (), None), (lambda a: a**2, (2,), 4), (lambda *a: a, [1, 2, 3], (1, 2, 3)), ], ) def test_can_proxy_lambdas(func, args, expected): @proxies(func) def wrapped(*args, **kwargs): return func(*args, **kwargs) assert wrapped.__name__ == "" assert wrapped(*args) == expected class Snowman: def __repr__(self): return "☃" class BittySnowman: def __repr__(self): return "☃" def test_can_handle_unicode_repr(): def foo(x): pass assert repr_call(foo, [Snowman()], {}) == "foo(x=☃)" assert repr_call(foo, [], {"x": Snowman()}) == "foo(x=☃)" class NoRepr: pass def test_can_handle_repr_on_type(): def foo(x): pass assert repr_call(foo, [Snowman], {}) == "foo(x=Snowman)" assert repr_call(foo, [NoRepr], {}) == "foo(x=NoRepr)" def test_can_handle_repr_of_none(): def foo(x): pass assert repr_call(foo, [None], {}) == "foo(x=None)" assert repr_call(foo, [], {"x": None}) == "foo(x=None)" def test_kwargs_appear_in_arg_string(): def varargs(*args, **kwargs): pass assert "x=1" in repr_call(varargs, (), {"x": 1}) def test_is_mock_with_negative_cases(): assert not is_mock(None) assert not is_mock(1234) assert not is_mock(is_mock) assert not is_mock(BittySnowman()) assert not is_mock("foobar") assert not is_mock(Mock(spec=BittySnowman)) assert not is_mock(MagicMock(spec=BittySnowman)) def test_is_mock_with_positive_cases(): assert is_mock(Mock()) assert is_mock(MagicMock()) assert is_mock(NonCallableMock()) assert is_mock(NonCallableMagicMock()) class Target: def __init__(self, a, b): pass def method(self, a, b): pass @pytest.mark.parametrize("target", [Target, Target(1, 2).method]) @pytest.mark.parametrize( "args,kwargs,expected", [ ((), {}, set("ab")), ((1,), {}, set("b")), ((1, 2), {}, set()), ((), {"a": 1}, set("b")), ((), {"b": 2}, set("a")), ((), {"a": 1, "b": 2}, set()), ], ) def test_required_args(target, args, kwargs, expected): # Mostly checking that `self` (and only self) is correctly excluded assert required_args(target, args, kwargs) == expected def test_can_handle_unicode_identifier_in_same_line_as_lambda_def(): # fmt: off pi = "π"; is_str_pi = lambda x: x == pi # noqa: E702 # fmt: on assert get_pretty_function_description(is_str_pi) == "lambda x: x == pi" def test_too_many_posargs_fails(): with pytest.raises(TypeError): st.times(time.min, time.max, st.none(), st.none()).validate() def test_overlapping_posarg_kwarg_fails(): with pytest.raises(TypeError): st.times(time.min, time.max, st.none(), timezones=st.just(None)).validate() def test_inline_given_handles_self(): # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/961 class Cls: def method(self, **kwargs): assert isinstance(self, Cls) assert kwargs["k"] is sentinel sentinel = object() given(k=st.just(sentinel))(Cls().method)() def logged(f): @wraps(f) def wrapper(*a, **kw): return f(*a, **kw) return wrapper class Bar: @logged def __init__(self, i: int): pass @given(st.builds(Bar)) def test_issue_2495_regression(_): """See https://github.com/HypothesisWorks/hypothesis/issues/2495""" @pytest.mark.skipif( sys.version_info[:2] >= (3, 11), reason="handled upstream in https://github.com/python/cpython/pull/92065", ) def test_error_on_keyword_parameter_name(): def f(source): pass f.__signature__ = Signature( parameters=[Parameter("from", Parameter.KEYWORD_ONLY)], return_annotation=Parameter.empty, ) with pytest.raises(ValueError, match="SyntaxError because `from` is a keyword"): get_signature(f) def test_param_is_called_within_func(): def f(any_name): any_name() assert is_first_param_referenced_in_function(f) def test_param_is_called_within_subfunc(): def f(any_name): def f2(): any_name() assert is_first_param_referenced_in_function(f) def test_param_is_not_called_within_func(): def f(any_name): pass assert not is_first_param_referenced_in_function(f) def test_param_called_within_defaults_on_error(): # Create a function object for which we cannot retrieve the source. f = compile("lambda: ...", "_.py", "eval") assert is_first_param_referenced_in_function(f) def _prep_source(*pairs): return [ pytest.param( dedent(x).strip(), dedent(y).strip().encode(), id=f"case-{i}", marks=marks ) for i, (x, y, *marks) in enumerate(pairs) ] @pytest.mark.parametrize( "src, clean", _prep_source( ("", ""), ("def test(): pass", "def test(): pass"), ("def invalid syntax", "def invalid syntax"), ("def also invalid(", "def also invalid("), ( """ @example(1) @given(st.integers()) def test(x): # line comment assert x # end-of-line comment "Had some blank lines above" """, """ def test(x): assert x "Had some blank lines above" """, ), ( """ @dec async def f(): pass """, """ async def f(): pass """, ), ), ) def test_clean_source(src, clean): assert reflection._clean_source(src).splitlines() == clean.splitlines() def test_overlong_repr_warns(): with pytest.warns(HypothesisWarning, match="overly large"): repr(LazyStrategy(st.one_of, [st.none()] * 10000, {})) def identity(x): return x class Identity: def __call__(self, x): return x def instance_identity(self, x): return x def instance_self(self): return self @staticmethod def static_identity(x): return x @classmethod def class_identity(cls, x): return x @pytest.mark.parametrize( "f", [ lambda x: x, identity, Identity(), Identity().instance_identity, Identity.static_identity, Identity.class_identity, ], ) def test_is_identity(f): assert is_identity_function(f) @example(Identity().instance_self) @example(lambda x, y: x) @example(lambda: None) @given(st.functions(like=identity, returns=st.from_type(type).flatmap(st.from_type))) def test_is_not_identity(f): obj = object() try: assume(f(obj) is not obj) # not necessary but ... except TypeError: pass assert not is_identity_function(f) @pytest.mark.parametrize( "f", [ lambda x, y=None: x, # this one would be fairly easy to recognize lambda x, y=None: y or x, # ... but the general case is impossible ], ) def test_is_unrecognized_identity(f): assert not is_identity_function(f) def test_cache_key_size_is_bounded(): # Modify co_consts because ("c" * 1000) may not be evaluated at compile time f = lambda x="a" * 1000, *, y="b" * 1000: "c" f.__code__ = f.__code__.replace( co_consts=tuple(c * 1000 if c == "c" else c for c in f.__code__.co_consts) ) assert len(repr(lambda_sources._function_key(f))) > 3000 assert len(repr(lambda_sources._function_key(f, bounded_size=True))) < 1000 def test_function_key_distinguishes_alpha_renames(): # these terms are equivalent under the lambda calculus, but their # representations are not, so they should be cached differently. assert lambda_sources._function_key(lambda x: x) != lambda_sources._function_key( lambda y: y ) def test_import(): import time as t f = lambda: t.ctime() assert get_pretty_function_description(f) == "lambda: t.ctime()" @pytest.mark.parametrize("nop_on_f", [True, False]) def test_code_normalization(nop_on_f): f = lambda x: x g = lambda x: x h = f if nop_on_f else g assert lambda_sources._function_key(f) == lambda_sources._function_key(g) # Append a NOP to one of the bytecodes NOP_instr = bytes([lambda_sources._op.NOP, 0]) h.__code__ = h.__code__.replace(co_code=h.__code__.co_code + NOP_instr) assert lambda_sources._function_key(f) != lambda_sources._function_key(g) # ...and then normalize g to match f (adding or removing a NOP) g.__code__ = lambda_sources._normalize_code(f, g) assert lambda_sources._function_key(f) == lambda_sources._function_key(g) @pytest.mark.parametrize( "f, source", [ (lambda x=1, *, y=2: (x, y), "lambda x=1, *, y=2: (x, y)"), (lambda x=1, *, y: (x, y), "lambda x=1, *, y: (x, y)"), (lambda x, *, y=2: (x, y), "lambda x, *, y=2: (x, y)"), (lambda x, *, y: (x, y), "lambda x, *, y: (x, y)"), (lambda *, y=2: (x, y), "lambda *, y=2: (x, y)"), # noqa: F821 (lambda *, y: (x, y), "lambda *, y: (x, y)"), # noqa: F821 (lambda x, /, y=1: (x, y), "lambda x, /, y=1: (x, y)"), (lambda x, /, y: (x, y), "lambda x, /, y: (x, y)"), (lambda x, /: (x, y), "lambda x, /: (x, y)"), # noqa: F821 (lambda x=1, /, y=2: (x, y), "lambda x=1, /, y=2: (x, y)"), (lambda x=1, /: (x, y), "lambda x=1, /: (x, y)"), # noqa: F821 ], ) def test_lambda_mimicry_with_arg_defaults(f, source): assert get_pretty_function_description(f) == source ================================================ FILE: hypothesis-python/tests/cover/test_regex.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import sys import unicodedata import pytest from hypothesis import HealthCheck, assume, given, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.internal.compat import PYPY from hypothesis.strategies._internal.regex import ( SPACE_CHARS, UNICODE_DIGIT_CATEGORIES, UNICODE_SPACE_CATEGORIES, UNICODE_SPACE_CHARS, UNICODE_WORD_CATEGORIES, base_regex_strategy, regex_strategy, ) from tests.common.debug import ( assert_all_examples, assert_no_examples, check_can_generate_examples, find_any, ) def is_ascii(s): return all(ord(c) < 128 for c in s) def is_digit(s): return all(unicodedata.category(c) in UNICODE_DIGIT_CATEGORIES for c in s) def is_space(s): return all(c in SPACE_CHARS for c in s) def is_unicode_space(s): return all( unicodedata.category(c) in UNICODE_SPACE_CATEGORIES or c in UNICODE_SPACE_CHARS for c in s ) def is_word(s): return all( c == "_" or unicodedata.category(c) in UNICODE_WORD_CATEGORIES for c in s ) def ascii_regex(pattern): return re.compile(pattern, re.ASCII) def unicode_regex(pattern): return re.compile(pattern, re.UNICODE) def _test_matching_pattern(pattern, *, isvalidchar, is_unicode=False): r = unicode_regex(pattern) if is_unicode else ascii_regex(pattern) codepoints = range(sys.maxunicode + 1) if is_unicode else range(1, 128) for c in [chr(x) for x in codepoints]: if isvalidchar(c): msg = "%r supposed to match %r (%r, category %r), but it doesn't" assert r.search(c), msg % (pattern, c, c, unicodedata.category(c)) else: assert not r.search(c), ( f'"{pattern}" supposed not to match {c!r} (category ' f"{unicodedata.category(c)!r}), but it does" ) @pytest.mark.parametrize( "category,predicate", [(r"\w", is_word), (r"\d", is_digit), (r"\s", None)] ) @pytest.mark.parametrize("invert", [False, True]) @pytest.mark.parametrize("is_unicode", [False, True]) def test_matching(category, predicate, invert, is_unicode): if predicate is None: # Special behaviour due to \x1c, INFORMATION SEPARATOR FOUR predicate = is_unicode_space if is_unicode else is_space if invert: category = category.swapcase() def pred(s): return not predicate(s) else: pred = predicate _test_matching_pattern(category, isvalidchar=pred, is_unicode=is_unicode) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~30s each; CrossHair strings just aren't fast enough", ) @pytest.mark.parametrize( "pattern", [ ".", # anything "a", "abc", "[a][b][c]", "[^a][^b][^c]", # literals "[a-z0-9_]", "[^a-z0-9_]", # range and negative range "ab?", "ab*", "ab+", # quantifiers "ab{5}", "ab{5,10}", "ab{,10}", "ab{5,}", # repeaters "ab|cd|ef", # branch "(foo)+", "(['\"])[a-z]+\\1", "(?:[a-z])(['\"])[a-z]+\\1", "(?P['\"])[a-z]+(?P=foo)", # groups "^abc", # beginning "\\d", "[\\d]", "[^\\D]", "\\w", "[\\w]", "[^\\W]", "\\s", "[\\s]", "[^\\S]", # categories ], ) @pytest.mark.parametrize("encode", [None, False, True]) def test_can_generate(pattern, encode): alphabet = st.characters(max_codepoint=1000) if encode is None else None if encode: pattern = pattern.encode("ascii") assert_all_examples( st.from_regex(pattern, alphabet=alphabet), re.compile(pattern).search, settings=settings(suppress_health_check=[HealthCheck.data_too_large]), ) @pytest.mark.parametrize( "pattern", [ re.compile("\\Aa\\Z", re.IGNORECASE), "(?i)\\Aa\\Z", re.compile("\\A[ab]\\Z", re.IGNORECASE), "(?i)\\A[ab]\\Z", ], ) def test_literals_with_ignorecase(pattern): strategy = st.from_regex(pattern) find_any(strategy, lambda s: s == "a") find_any(strategy, lambda s: s == "A") @pytest.mark.parametrize( "pattern", [re.compile("\\A[^a][^b]\\Z", re.IGNORECASE), "(?i)\\A[^a][^b]\\Z"] ) def test_not_literal_with_ignorecase(pattern): assert_all_examples( st.from_regex(pattern), lambda s: s[0] not in ("a", "A") and s[1] not in ("b", "B"), ) def test_any_doesnt_generate_newline(): assert_all_examples(st.from_regex("\\A.\\Z"), lambda s: s != "\n") @pytest.mark.parametrize("pattern", [re.compile("\\A.\\Z", re.DOTALL), "(?s)\\A.\\Z"]) def test_any_with_dotall_generate_newline(pattern): find_any(st.from_regex(pattern), lambda s: s == "\n", settings(max_examples=10**6)) @pytest.mark.parametrize("pattern", [re.compile(b"\\A.\\Z", re.DOTALL), b"(?s)\\A.\\Z"]) def test_any_with_dotall_generate_newline_binary(pattern): find_any(st.from_regex(pattern), lambda s: s == b"\n", settings(max_examples=10**6)) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~30s each; CrossHair strings just aren't fast enough", ) @pytest.mark.parametrize( "pattern", ["\\d", "[\\d]", "[^\\D]", "\\w", "[\\w]", "[^\\W]", "\\s", "[\\s]", "[^\\S]"], ) @pytest.mark.parametrize("is_unicode", [False, True]) @pytest.mark.parametrize("invert", [False, True]) def test_groups(pattern, is_unicode, invert): if "d" in pattern.lower(): group_pred = is_digit elif "w" in pattern.lower(): group_pred = is_word else: # Special behaviour due to \x1c, INFORMATION SEPARATOR FOUR group_pred = is_unicode_space if is_unicode else is_space if invert: pattern = pattern.swapcase() _p = group_pred def group_pred(s): return not _p(s) pattern = f"^{pattern}\\Z" compiler = unicode_regex if is_unicode else ascii_regex strategy = st.from_regex(compiler(pattern)) find_any(strategy.filter(group_pred), is_ascii) if is_unicode: find_any(strategy, lambda s: group_pred(s) and not is_ascii(s)) assert_all_examples(strategy, group_pred) def test_caret_in_the_middle_does_not_generate_anything(): r = re.compile("a^b") assert_no_examples(st.from_regex(r)) def test_end_with_terminator_does_not_pad(): assert_all_examples(st.from_regex("abc\\Z"), lambda x: x[-3:] == "abc") def test_end(): strategy = st.from_regex("\\Aabc$") find_any(strategy, lambda s: s == "abc") find_any(strategy, lambda s: s == "abc\n") def test_groupref_exists(): assert_all_examples( st.from_regex("^(<)?a(?(1)>)$"), lambda s: s in ("a", "a\n", "", "\n") ) assert_all_examples( st.from_regex("^(a)?(?(1)b|c)$"), lambda s: s in ("ab", "ab\n", "c", "c\n") ) def test_impossible_negative_lookahead(): assert_no_examples(st.from_regex("(?!foo)foo")) @given(st.from_regex("(\\Afoo\\Z)")) def test_can_handle_boundaries_nested(s): assert s == "foo" def test_groupref_not_shared_between_regex(): # If group references are (incorrectly!) shared between regex, this would # fail as the would only be one reference. check_can_generate_examples( st.tuples(st.from_regex("(a)\\1"), st.from_regex("(b)\\1")) ) @pytest.mark.skipif( PYPY, # Skip for now so we can test the rest reason=r"Triggers bugs in poor handling of unicode in re for these implementations", ) @given(st.data()) def test_group_ref_is_not_shared_between_identical_regex(data): pattern = re.compile("^(.+)\\1\\Z", re.UNICODE) x = data.draw(base_regex_strategy(pattern, alphabet=st.characters())) y = data.draw(base_regex_strategy(pattern, alphabet=st.characters())) assume(x != y) assert pattern.match(x).end() == len(x) assert pattern.match(y).end() == len(y) @given(st.data()) def test_does_not_leak_groups(data): a = data.draw(base_regex_strategy(re.compile("^(a)\\Z"), alphabet=st.characters())) assert a == "a" b = data.draw( base_regex_strategy(re.compile("^(?(1)a|b)(.)\\Z"), alphabet=st.characters()) ) assert b[0] == "b" def test_positive_lookbehind(): find_any(st.from_regex(".*(?<=ab)c"), lambda s: s.endswith("abc")) def test_positive_lookahead(): find_any(st.from_regex("a(?=bc).*"), lambda s: s.startswith("abc")) def test_negative_lookbehind(): # no efficient support strategy = st.from_regex("[abc]*(? 1, ) def test_internals_can_disable_newline_from_dollar_for_jsonschema(): pattern = "^abc$" find_any(st.from_regex(pattern), lambda s: s == "abc\n") assert_all_examples( regex_strategy( pattern, False, alphabet=st.characters(), _temp_jsonschema_hack_no_end_newline=True, ), lambda s: s == "abc", ) @given(st.from_regex(r"[^.].*", alphabet=st.sampled_from("abc") | st.just("."))) def test_can_pass_union_for_alphabet(_): pass ================================================ FILE: hypothesis-python/tests/cover/test_regressions.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pickle import random from datetime import timedelta from unittest.mock import Mock import pytest from hypothesis import ( Verbosity, assume, errors, given, seed, settings, strategies as st, ) from hypothesis.internal import compat from hypothesis.internal.escalation import InterestingOrigin from tests.common.utils import skipif_threading def strat(): return st.builds(dict, one=strat_one()) @st.composite def strat_one(draw): return draw(st.builds(dict, val=st.builds(dict, two=strat_two()))) @st.composite def strat_two(draw): return draw(st.builds(dict, some_text=st.text(min_size=1))) @given(strat()) def test_issue751(v): pass def test_can_find_non_zero(): # This future proofs against a possible failure mode where the depth bound # is triggered but we've fixed the behaviour of min_size so that it can # handle that: We want to make sure that we're really not depth bounding # the text in the leaf nodes. @settings(verbosity=Verbosity.quiet) @given(strat()) def test(v): assert "0" in v["one"]["val"]["two"]["some_text"] with pytest.raises(AssertionError): test() def test_mock_injection(): """Ensure that it's possible for mechanisms like `pytest.fixture` and `patch` to inject mocks into hypothesis test functions without side effects. (covers https://github.com/HypothesisWorks/hypothesis- python/issues/491) """ class Bar: pass @given(inp=st.integers()) def test_foo_spec(bar, inp): pass test_foo_spec(Bar()) test_foo_spec(Mock(Bar)) test_foo_spec(Mock()) def test_regression_issue_1230(): strategy = st.builds( lambda x, y: dict(list(x.items()) + list(y.items())), st.fixed_dictionaries({"0": st.text()}), st.builds( lambda dictionary, keys: {key: dictionary[key] for key in keys}, st.fixed_dictionaries({"1": st.lists(elements=st.sampled_from(["a"]))}), st.sets(elements=st.sampled_from(["1"])), ), ) @seed(81581571036124932593143257836081491416) @settings(database=None) @given(strategy) def test_false_is_false(params): assume(params.get("0") not in ("", "\x00")) raise ValueError with pytest.raises(ValueError): test_false_is_false() @given(st.integers()) def random_func(x): random.random() @skipif_threading def test_prng_state_unpolluted_by_given_issue_1266(): # Checks that @given doesn't leave the global PRNG in a particular # modified state; there may be no effect or random effect but not # a consistent end-state. first = random.getstate() random_func() second = random.getstate() random_func() third = random.getstate() if first == second: assert second == third else: assert second != third exc_instances = [ errors.NoSuchExample("foobar", extra="baz"), errors.DeadlineExceeded( runtime=timedelta(seconds=1.5), deadline=timedelta(seconds=1.0) ), errors.RewindRecursive(int), errors.UnsatisfiedAssumption("reason for unsatisfied"), errors.FlakyReplay( "reason", interesting_origins=[InterestingOrigin.from_exception(BaseException())], ), errors.FlakyFailure("check with BaseException", [BaseException()]), errors.BackendCannotProceed("verified"), ] @pytest.mark.parametrize("exc", exc_instances, ids=repr) def test_exceptions_are_picklable(exc): # See https://github.com/HypothesisWorks/hypothesis/issues/3426 pickle.loads(pickle.dumps(exc)) def test_no_missed_custom_init_exceptions(): untested_errors_with_custom_init = { et for et in vars(errors).values() if isinstance(et, type) and et not in vars(compat).values() # skip types imported for compatibility and issubclass(et, Exception) and ("__init__" in vars(et) or "__new__" in vars(et)) } - {type(exc) for exc in exc_instances} print(untested_errors_with_custom_init) assert not untested_errors_with_custom_init ================================================ FILE: hypothesis-python/tests/cover/test_replay_logic.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, settings, strategies as st from hypothesis.database import InMemoryExampleDatabase from hypothesis.internal.compat import ExceptionGroup def test_does_not_shrink_on_replay(): database = InMemoryExampleDatabase() call_count = 0 is_first = True last = None @settings( database=database, report_multiple_bugs=False, derandomize=False, max_examples=1000, ) @given(st.lists(st.integers(), unique=True, min_size=3)) def test(ls): nonlocal call_count, is_first, last if is_first and last is not None: assert ls == last is_first = False last = ls call_count += 1 raise AssertionError with pytest.raises(AssertionError): test() assert last is not None call_count = 0 is_first = True with pytest.raises(AssertionError): test() assert call_count == 2 def test_does_not_shrink_on_replay_with_multiple_bugs(): database = InMemoryExampleDatabase() call_count = 0 raised = False marker = 1000093 @settings( database=database, report_multiple_bugs=True, derandomize=False, max_examples=1000, ) @given(st.integers()) def test(n): nonlocal call_count, raised call_count += 1 if n >= marker: raised = True raise AssertionError elif n < marker and raised: raise AssertionError with pytest.raises(ExceptionGroup): test() call_count = 0 with pytest.raises(ExceptionGroup): test() assert call_count == 4 def test_will_always_shrink_if_previous_example_does_not_replay(): database = InMemoryExampleDatabase() good = set() last = None @settings( database=database, report_multiple_bugs=True, derandomize=False, max_examples=1000, ) @given(st.integers(min_value=0)) def test(n): nonlocal last if n not in good: last = n raise AssertionError for i in range(20): with pytest.raises(AssertionError): test() assert last == i good.add(last) def test_will_shrink_if_the_previous_example_does_not_look_right(): database = InMemoryExampleDatabase() last = None first_test = True @settings(database=database, report_multiple_bugs=True, derandomize=False) @given(st.data()) def test(data): nonlocal last m = data.draw(st.integers()) last = m if first_test: data.draw(st.integers()) assert m < 10000 else: raise AssertionError with pytest.raises(AssertionError): test() assert last is not None assert last > 0 first_test = False with pytest.raises(AssertionError): test() assert last == 0 ================================================ FILE: hypothesis-python/tests/cover/test_reporting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import sys import pytest from hypothesis import given, reporting from hypothesis._settings import Verbosity, settings from hypothesis.reporting import debug_report, verbose_report from hypothesis.strategies import integers from tests.common.utils import capture_out def test_prints_output_by_default(): @given(integers()) def test_int(x): raise AssertionError with pytest.raises(AssertionError) as err: test_int() assert "Falsifying example" in "\n".join(err.value.__notes__) def test_does_not_print_debug_in_verbose(): @given(integers()) @settings(verbosity=Verbosity.verbose) def f(x): debug_report("Hi") with capture_out() as o: f() assert "Hi" not in o.getvalue() def test_does_print_debug_in_debug(): @given(integers()) @settings(verbosity=Verbosity.debug) def f(x): debug_report("Hi") with capture_out() as o: f() assert "Hi" in o.getvalue() def test_does_print_verbose_in_debug(): @given(integers()) @settings(verbosity=Verbosity.debug) def f(x): verbose_report("Hi") with capture_out() as o: f() assert "Hi" in o.getvalue() def test_can_report_when_system_locale_is_ascii(monkeypatch): read, write = os.pipe() with ( open(read, encoding="ascii") as read, open(write, "w", encoding="ascii") as write, ): monkeypatch.setattr(sys, "stdout", write) reporting.default("☃") ================================================ FILE: hypothesis-python/tests/cover/test_reproduce_failure.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import base64 import re import zlib import pytest from hypothesis import ( Verbosity, __version__, example, given, reject, reproduce_failure, settings, strategies as st, ) from hypothesis.core import decode_failure, encode_failure from hypothesis.errors import DidNotReproduce, InvalidArgument, UnsatisfiedAssumption from hypothesis.internal.conjecture.choice import choice_equal from tests.common.utils import Why, capture_out, no_shrink, xfail_on_crosshair from tests.conjecture.common import nodes, nodes_inline @example(nodes_inline("0" * 100)) # shorter compressed than not @given(st.lists(nodes())) def test_encoding_loop(nodes): choices = [n.value for n in nodes] looped = decode_failure(encode_failure(choices)) assert len(choices) == len(looped) for pre, post in zip(choices, looped, strict=True): assert choice_equal(pre, post) @example(base64.b64encode(b"\2\3\4")) @example(b"\t") @example(base64.b64encode(b"\1\0")) # zlib error @example(base64.b64encode(b"\1" + zlib.compress(b"\xff"))) # choices_from_bytes error @given(st.binary()) def test_decoding_may_fail(t): try: decode_failure(t) reject() except UnsatisfiedAssumption: raise # don't silence the reject() except InvalidArgument: pass except Exception as e: raise AssertionError("Expected an InvalidArgument exception") from e def test_invalid_base_64_gives_invalid_argument(): with pytest.raises(InvalidArgument) as exc_info: decode_failure(b"/") assert "Invalid base64 encoded" in exc_info.value.args[0] def test_reproduces_the_failure(): b = b"hello world" n = len(b) @reproduce_failure(__version__, encode_failure([b])) @given(st.binary(min_size=n, max_size=n)) def test_outer(x): assert x != b @given(st.binary(min_size=n, max_size=n)) @reproduce_failure(__version__, encode_failure([b])) def test_inner(x): assert x != b with pytest.raises(AssertionError): test_outer() with pytest.raises(AssertionError): test_inner() def test_errors_if_provided_example_does_not_reproduce_failure(): b = b"hello world" n = len(b) @reproduce_failure(__version__, encode_failure([b])) @given(st.binary(min_size=n, max_size=n)) def test(x): assert x == b with pytest.raises(DidNotReproduce): test() def test_errors_with_did_not_reproduce_if_the_shape_changes(): b = b"hello world" n = len(b) @reproduce_failure(__version__, encode_failure([b])) @given(st.binary(min_size=n, max_size=n) | st.integers()) def test(v): assert v == b with pytest.raises(DidNotReproduce): test() def test_errors_with_did_not_reproduce_if_rejected(): b = b"hello world" n = len(b) @reproduce_failure(__version__, encode_failure([b])) @given(st.binary(min_size=n, max_size=n)) def test(x): reject() with pytest.raises(DidNotReproduce): test() @xfail_on_crosshair(Why.symbolic_outside_context) def test_prints_reproduction_if_requested(): failing_example = None @settings(print_blob=True, database=None, max_examples=100) @given(st.integers()) def test(i): nonlocal failing_example if failing_example is None and i != 0: failing_example = i assert i != failing_example with pytest.raises(AssertionError) as err: test() notes = "\n".join(err.value.__notes__) assert "@reproduce_failure" in notes exp = re.compile(r"reproduce_failure\(([^)]+)\)", re.MULTILINE) extract = exp.search(notes) reproduction = eval(extract.group(0)) test = reproduction(test) with pytest.raises(AssertionError): test() def test_does_not_print_reproduction_for_simple_examples_by_default(): @settings(print_blob=False) @given(st.integers()) def test(i): raise AssertionError with capture_out() as o, pytest.raises(AssertionError): test() assert "@reproduce_failure" not in o.getvalue() def test_does_not_print_reproduction_for_simple_data_examples_by_default(): @settings(print_blob=False) @given(st.data()) def test(data): data.draw(st.integers()) raise AssertionError with capture_out() as o, pytest.raises(AssertionError): test() assert "@reproduce_failure" not in o.getvalue() def test_does_not_print_reproduction_for_large_data_examples_by_default(): @settings(phases=no_shrink, print_blob=False) @given(st.data()) def test(data): b = data.draw(st.binary(min_size=1000, max_size=1000)) if len(zlib.compress(b)) > 1000: raise ValueError with capture_out() as o, pytest.raises(ValueError): test() assert "@reproduce_failure" not in o.getvalue() class Foo: def __repr__(self): return "not a valid python expression" def test_does_not_print_reproduction_if_told_not_to(): @settings(print_blob=False) @given(st.integers().map(lambda x: Foo())) def test(i): raise ValueError with capture_out() as o, pytest.raises(ValueError): test() assert "@reproduce_failure" not in o.getvalue() def test_raises_invalid_if_wrong_version(): b = b"hello world" n = len(b) @reproduce_failure("1.0.0", encode_failure([b])) @given(st.binary(min_size=n, max_size=n)) def test(x): pass with pytest.raises(InvalidArgument): test() def test_does_not_print_reproduction_if_verbosity_set_to_quiet(): @given(st.data()) @settings(verbosity=Verbosity.quiet, print_blob=False) def test_always_fails(data): assert data.draw(st.just(False)) with capture_out() as out, pytest.raises(AssertionError): test_always_fails() assert "@reproduce_failure" not in out.getvalue() ================================================ FILE: hypothesis-python/tests/cover/test_runner_strategy.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from unittest import TestCase import pytest from hypothesis import find, given, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.stateful import RuleBasedStateMachine, rule def test_cannot_use_without_a_runner(): @given(st.runner()) def f(x): pass with pytest.raises(InvalidArgument): f() def test_cannot_use_in_find_without_default(): with pytest.raises(InvalidArgument): find(st.runner(), lambda x: True) def test_is_default_in_find(): t = object() assert find(st.runner(default=t), lambda x: True) == t @given(st.runner(default=1)) def test_is_default_without_self(runner): assert runner == 1 class TestStuff(TestCase): @given(st.runner()) def test_runner_is_self(self, runner): assert runner is self @given(st.runner(default=3)) def test_runner_is_self_even_with_default(self, runner): assert runner is self class RunnerStateMachine(RuleBasedStateMachine): @rule(runner=st.runner()) def step(self, runner): assert runner is self TestState = RunnerStateMachine.TestCase ================================================ FILE: hypothesis-python/tests/cover/test_sampled_from.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import collections import enum import operator import pytest from hypothesis import given, settings, strategies as st from hypothesis.errors import ( FailedHealthCheck, InvalidArgument, StopTest, Unsatisfiable, ) from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.strategies import sampled_from from hypothesis.strategies._internal.misc import JustStrategy from hypothesis.strategies._internal.strategies import ( FilteredStrategy, filter_not_satisfied, ) from tests.common.debug import ( assert_all_examples, assert_simple_property, check_can_generate_examples, ) from tests.common.utils import fails_with an_enum = enum.Enum("A", "a b c") a_flag = enum.Flag("A", "a b c") # named zero state is required for empty flags from around py3.11/3.12 an_empty_flag = enum.Flag("EmptyFlag", {"a": 0}) an_ordereddict = collections.OrderedDict([("a", 1), ("b", 2), ("c", 3)]) @fails_with(InvalidArgument) def test_cannot_sample_sets(): check_can_generate_examples(sampled_from(set("abc"))) def test_can_sample_sequence_without_warning(): check_can_generate_examples(sampled_from([1, 2, 3])) def test_can_sample_ordereddict_without_warning(): check_can_generate_examples(sampled_from(an_ordereddict)) @pytest.mark.parametrize("enum_class", [an_enum, a_flag, an_empty_flag]) def test_can_sample_enums(enum_class): assert_all_examples(sampled_from(enum_class), lambda x: isinstance(x, enum_class)) @fails_with(FailedHealthCheck) @settings(suppress_health_check=[]) @given(sampled_from(range(10)).filter(lambda x: x < 0)) def test_unsat_filtered_sampling(x): raise AssertionError @fails_with(Unsatisfiable) @settings(suppress_health_check=[]) @given(sampled_from(range(2)).filter(lambda x: x < 0)) def test_unsat_filtered_sampling_in_rejection_stage(x): # Rejecting all possible indices before we calculate the allowed indices # takes an early exit path, so we need this test to cover that branch. raise AssertionError def test_easy_filtered_sampling(): assert_simple_property( sampled_from(range(100)).filter(lambda x: x == 0), lambda x: x == 0 ) @given(sampled_from(range(100)).filter(lambda x: x == 99)) def test_filtered_sampling_finds_rare_value(x): assert x == 99 @given(st.sets(st.sampled_from(range(50)), min_size=50)) def test_efficient_sets_of_samples(x): assert x == set(range(50)) @given(st.dictionaries(keys=st.sampled_from(range(50)), values=st.none(), min_size=50)) def test_efficient_dicts_with_sampled_keys(x): assert set(x) == set(range(50)) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes 3-5 mins and raises Unsatisfiable", ) @pytest.mark.parametrize( "fn", [lambda asdf: asdf[0], (operator.itemgetter(0), lambda x: x[0])] ) @given(st.data()) def test_efficient_lists_of_tuples_first_element_sampled_from(fn, data): elems = st.tuples(st.sampled_from(range(20)), st.builds(list)) x = data.draw(st.lists(elems, min_size=20, unique_by=fn)) assert {first for first, *_ in x} == set(range(20)) @given(st.lists(st.sampled_from([0] * 100), unique=True)) def test_does_not_include_duplicates_even_when_duplicated_in_collection(ls): assert len(ls) <= 1 @given( st.sets( st.sampled_from(range(50)) .map(lambda x: x * 2) .filter(lambda x: x % 3) .map(lambda x: x // 2), min_size=33, ) ) def test_efficient_sets_of_samples_with_chained_transformations(x): assert x == {x for x in range(50) if (x * 2) % 3} @st.composite def stupid_sampled_sets(draw): result = set() s = st.sampled_from(range(20)).filter(lambda x: x % 3).map(lambda x: x * 2) while len(result) < 13: result.add(draw(s.filter(lambda x: x not in result))) return result @given(stupid_sampled_sets()) def test_efficient_sets_of_samples_with_chained_transformations_slow_path(x): # This deliberately exercises the standard filtering logic without going # through the special-case handling of UniqueSampledListStrategy. assert x == {x * 2 for x in range(20) if x % 3} @fails_with(Unsatisfiable) @given(FilteredStrategy(st.sampled_from([None, False, ""]), conditions=(bool,))) def test_unsatisfiable_explicit_filteredstrategy_sampled(x): raise AssertionError("Unreachable because there are no valid examples") @fails_with(Unsatisfiable) @given(FilteredStrategy(st.none(), conditions=(bool,))) def test_unsatisfiable_explicit_filteredstrategy_just(x): raise AssertionError("Unreachable because there are no valid examples") def test_transformed_just_strategy(): data = ConjectureData.for_choices([]) s = JustStrategy([1]).map(lambda x: x * 2) assert s.do_draw(data) == 2 sf = s.filter(lambda x: False) assert isinstance(sf, JustStrategy) assert sf.do_filtered_draw(data) == filter_not_satisfied with pytest.raises(StopTest): sf.do_draw(data) @given(st.lists(st.sampled_from(range(100)), max_size=3, unique=True)) def test_max_size_is_respected_with_unique_sampled_from(ls): assert len(ls) <= 3 @given(st.lists(st.sampled_from([0, 0.0]), unique=True, min_size=1)) def test_issue_2247_regression(ls): assert len(ls) == 1 @given(st.data()) def test_mutability_1(data): # See https://github.com/HypothesisWorks/hypothesis/issues/2507 x = [1] s = st.sampled_from(x) x.append(2) assert data.draw(s) != 2 @given(st.data()) def test_mutability_2(data): x = [1] s = st.sampled_from(x) assert data.draw(s) != 2 x.append(2) assert data.draw(s) != 2 class AnnotationsInsteadOfElements(enum.Enum): a: "int" def test_suggests_elements_instead_of_annotations(): with pytest.raises(InvalidArgument, match=r"Cannot sample.*annotations.*dataclass"): check_can_generate_examples(st.sampled_from(AnnotationsInsteadOfElements)) class TestErrorNoteBehavior3819: elements = (st.booleans(), st.decimals(), st.integers(), st.text()) @staticmethod @given(st.data()) def direct_without_error(data): data.draw(st.sampled_from((st.floats(), st.binary()))) @staticmethod @given(st.data()) def direct_with_non_type_error(data): data.draw(st.sampled_from(st.characters(), st.floats())) raise Exception("Contains SearchStrategy, but no note addition!") @staticmethod @given(st.data()) def direct_with_type_error_without_substring(data): data.draw(st.sampled_from(st.booleans(), st.binary())) raise TypeError("Substring not in message!") @staticmethod @given(st.data()) def direct_with_type_error_with_substring_but_not_all_strategies(data): data.draw(st.sampled_from(st.booleans(), False, True)) raise TypeError("Contains SearchStrategy, but no note addition!") @staticmethod @given(st.data()) def direct_all_strategies_with_type_error_with_substring(data): data.draw(st.sampled_from((st.dates(), st.datetimes()))) raise TypeError("This message contains SearchStrategy as substring!") @staticmethod @given(st.lists(st.sampled_from(elements))) def indirect_without_error(_): return @staticmethod @given(st.lists(st.sampled_from(elements))) def indirect_with_non_type_error(_): raise Exception("Contains SearchStrategy, but no note addition!") @staticmethod @given(st.lists(st.sampled_from(elements))) def indirect_with_type_error_without_substring(_): raise TypeError("Substring not in message!") @staticmethod @given(st.lists(st.sampled_from((*elements, False, True)))) def indirect_with_type_error_with_substring_but_not_all_strategies(_): raise TypeError("Contains SearchStrategy, but no note addition!") @staticmethod @given(st.lists(st.sampled_from(elements), min_size=1)) def indirect_all_strategies_with_type_error_with_substring(objs): raise TypeError("Contains SearchStrategy in message, trigger note!") @pytest.mark.parametrize( ["func_to_call", "exp_err_cls", "should_exp_msg"], [ pytest.param(f.__func__, err, msg_exp, id=f.__func__.__name__) for f, err, msg_exp in [ (f, TypeError, True) for f in ( direct_all_strategies_with_type_error_with_substring, indirect_all_strategies_with_type_error_with_substring, ) ] + [ (f, TypeError, False) for f in ( direct_with_type_error_without_substring, direct_with_type_error_with_substring_but_not_all_strategies, indirect_with_type_error_without_substring, indirect_with_type_error_with_substring_but_not_all_strategies, ) ] + [ (f, Exception, False) for f in ( direct_with_non_type_error, indirect_with_non_type_error, ) ] + [ (f, None, False) for f in ( direct_without_error, indirect_without_error, ) ] ], ) def test_error_appropriate_error_note_3819( self, func_to_call, exp_err_cls, should_exp_msg ): if exp_err_cls is None: # Here we only care that no exception was raised, so nothing to assert. func_to_call() else: with pytest.raises(exp_err_cls) as err_ctx: func_to_call() notes = getattr(err_ctx.value, "__notes__", []) matching_messages = [ n for n in notes if n.startswith("sampled_from was given a collection of strategies") and n.endswith("Was one_of intended?") ] assert len(matching_messages) == (1 if should_exp_msg else 0) ================================================ FILE: hypothesis-python/tests/cover/test_searchstrategy.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import functools import random import sys from collections import defaultdict, namedtuple from dataclasses import dataclass from typing import Any import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument, Unsatisfiable from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.utils import to_jsonable from hypothesis.utils.conventions import UniqueIdentifier from tests.common.debug import assert_simple_property, check_can_generate_examples from tests.common.utils import checks_deprecated_behaviour def test_or_errors_when_given_non_strategy(): bools = st.tuples(st.booleans()) with pytest.raises(ValueError): bools | "foo" SomeNamedTuple = namedtuple("SomeNamedTuple", ("a", "b")) def last(xs): t = None for x in xs: t = x return t def test_just_strategy_uses_repr(): class WeirdRepr: def __repr__(self): return "ABCDEFG" assert repr(st.just(WeirdRepr())) == f"just({WeirdRepr()!r})" def test_just_strategy_does_not_draw(): data = ConjectureData.for_choices([]) s = st.just("hello") assert s.do_draw(data) == "hello" def test_none_strategy_does_not_draw(): data = ConjectureData.for_choices([]) s = st.none() assert s.do_draw(data) is None def test_can_map(): s = st.integers().map(pack=lambda t: "foo") assert_simple_property(s, lambda v: v == "foo") def test_example_raises_unsatisfiable_when_too_filtered(): with pytest.raises(Unsatisfiable): check_can_generate_examples(st.integers().filter(lambda x: False)) def nameless_const(x): def f(u, v): return u return functools.partial(f, x) def test_can_map_nameless(): f = nameless_const(2) assert get_pretty_function_description(f) in repr(st.integers().map(f)) def test_can_flatmap_nameless(): f = nameless_const(st.just(3)) assert get_pretty_function_description(f) in repr(st.integers().flatmap(f)) def test_flatmap_with_invalid_expand(): with pytest.raises(InvalidArgument): check_can_generate_examples(st.just(100).flatmap(lambda n: "a")) _bad_random_strategy = st.lists(st.integers(), min_size=1).map(random.choice) @checks_deprecated_behaviour def test_use_of_global_random_is_deprecated_in_given(): check_can_generate_examples(_bad_random_strategy) @checks_deprecated_behaviour def test_use_of_global_random_is_deprecated_in_interactive_draws(): @given(st.data()) def inner(d): d.draw(_bad_random_strategy) inner() def test_jsonable(): assert to_jsonable(object(), avoid_realization=True) == "" assert isinstance(to_jsonable(object(), avoid_realization=False), str) @dataclass class HasDefaultDict: x: defaultdict def test_jsonable_defaultdict(): obj = HasDefaultDict(defaultdict(list)) obj.x["a"] = [42] assert to_jsonable(obj, avoid_realization=False) == {"x": {"a": [42]}} def test_jsonable_namedtuple(): Obj = namedtuple("Obj", ("x")) obj = Obj(10) assert to_jsonable(obj, avoid_realization=False) == {"x": 10} def test_jsonable_small_ints_are_ints(): n = 2**62 for avoid in (True, False): assert isinstance(to_jsonable(n, avoid_realization=avoid), int) assert to_jsonable(n, avoid_realization=avoid) == n def test_jsonable_large_ints_are_floats(): n = 2**63 assert isinstance(to_jsonable(n, avoid_realization=False), float) assert to_jsonable(n, avoid_realization=False) == float(n) assert to_jsonable(n, avoid_realization=True) == "" def test_jsonable_very_large_ints(): # previously caused OverflowError when casting to float. n = 2**1024 assert to_jsonable(n, avoid_realization=False) == sys.float_info.max assert to_jsonable(n, avoid_realization=True) == "" @dataclass class HasCustomJsonFormat: x: str def to_json(self): return "surprise!" def test_jsonable_override(): obj = HasCustomJsonFormat("expected") assert to_jsonable(obj, avoid_realization=False) == "surprise!" assert to_jsonable(obj, avoid_realization=True) == "" @dataclass class Inner: value: int def to_json(self): return "custom" @dataclass class Outer: inner: Inner def test_jsonable_to_json_nested(): obj = Outer(Inner(42)) assert to_jsonable(obj, avoid_realization=False) == {"inner": "custom"} assert to_jsonable(obj, avoid_realization=True) == "" recursive_list = [] recursive_list.append(recursive_list) recursive_dict = {} recursive_dict["a"] = recursive_dict mutual1 = [] mutual2 = [mutual1] mutual1.append(mutual2) shared = UniqueIdentifier("shared") @dataclass class A: a: Any b: Any @pytest.mark.parametrize( "obj, value", [ (recursive_list, ["[...]"]), (recursive_dict, {"a": "{...}"}), (mutual1, [["[...]"]]), (mutual2, [["[...]"]]), # same id object in different fields. no cycle (A(a=shared, b=shared), {"a": "shared", "b": "shared"}), (A(a=recursive_list, b=recursive_dict), {"a": ["[...]"], "b": {"a": "{...}"}}), ], ) def test_to_jsonable_handles_reference_cycles(obj, value): assert to_jsonable(obj, avoid_realization=False) == value def test_deferred_strategy_draw(): strategy = st.deferred(lambda: st.integers()) assert strategy.do_draw(ConjectureData.for_choices([0])) == 0 ================================================ FILE: hypothesis-python/tests/cover/test_seed_printing.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import time import pytest from hypothesis import Verbosity, assume, core, given, settings, strategies as st from hypothesis.database import InMemoryExampleDatabase from hypothesis.errors import FailedHealthCheck from tests.common.utils import all_values, capture_out @pytest.mark.parametrize("in_pytest", [False, True]) @pytest.mark.parametrize("fail_healthcheck", [False, True]) @pytest.mark.parametrize("verbosity", [Verbosity.normal, Verbosity.quiet]) def test_prints_seed_only_on_healthcheck( monkeypatch, in_pytest, fail_healthcheck, verbosity ): monkeypatch.setattr(core, "running_under_pytest", in_pytest) strategy = st.integers() if fail_healthcheck: def slow_map(i): time.sleep(10) return i strategy = strategy.map(slow_map) expected_exc = FailedHealthCheck else: expected_exc = AssertionError @settings(database=None, verbosity=verbosity, suppress_health_check=()) @given(strategy) def test(i): assert fail_healthcheck with capture_out() as o, pytest.raises(expected_exc): test() output = o.getvalue() seed = test._hypothesis_internal_use_generated_seed assert seed is not None if fail_healthcheck and verbosity != Verbosity.quiet: assert f"@seed({seed})" in output contains_pytest_instruction = f"--hypothesis-seed={seed}" in output assert contains_pytest_instruction == in_pytest else: assert "@seed" not in output assert f"--hypothesis-seed={seed}" not in output def test_uses_global_force(monkeypatch): monkeypatch.setattr(core, "global_force_seed", 42) @given(st.integers()) def test(i): raise ValueError output = [] for _ in range(2): with capture_out() as o, pytest.raises(ValueError): test() output.append(o.getvalue()) assert output[0] == output[1] assert "@seed" not in output[0] def test_does_print_on_reuse_from_database(): passes_healthcheck = False database = InMemoryExampleDatabase() @settings(database=database, suppress_health_check=[]) @given(st.integers()) def test(i): assume(passes_healthcheck) raise ValueError with capture_out() as o, pytest.raises(FailedHealthCheck): test() assert "@seed" in o.getvalue() passes_healthcheck = True with capture_out() as o, pytest.raises(ValueError): test() assert all_values(database) assert "@seed" not in o.getvalue() passes_healthcheck = False with capture_out() as o, pytest.raises(FailedHealthCheck): test() assert "@seed" in o.getvalue() ================================================ FILE: hypothesis-python/tests/cover/test_settings.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime import os import subprocess import sys from contextlib import contextmanager from threading import RLock from unittest import TestCase import pytest from hypothesis import example, given, strategies as st from hypothesis._settings import ( _CI_VARS, HealthCheck, Phase, Verbosity, default_variable, local_settings, settings, ) from hypothesis.database import InMemoryExampleDatabase from hypothesis.errors import ( HypothesisDeprecationWarning, InvalidArgument, ) from hypothesis.stateful import RuleBasedStateMachine, rule from hypothesis.utils.conventions import not_set from hypothesis.utils.deprecation import note_deprecation from tests.common.utils import ( checks_deprecated_behaviour, counts_calls, fails_with, skipif_emscripten, skipif_threading, validate_deprecation, ) original_default = settings.get_profile("default").max_examples _temp_register_profile_lock = RLock() @contextmanager def temp_register_profile(name, parent, **kwargs): with _temp_register_profile_lock: try: settings.register_profile(name, parent, **kwargs) yield finally: settings._profiles.pop(name) _restore_profile_lock = RLock() @contextmanager def restore_profile(): with _restore_profile_lock: # avoid polluting global state by resetting the loaded profile back to its # previous value in tests which use load_profile current_profile = settings.get_current_profile_name() try: yield finally: settings.load_profile(current_profile) def test_cannot_set_non_settings(): s = settings() with pytest.raises(AttributeError): s.databas_file = "some_file" def test_settings_uses_defaults(): s = settings() assert s.max_examples == settings.default.max_examples def test_raises_attribute_error(): with pytest.raises(AttributeError): settings().kittens def test_respects_none_database(): assert settings(database=None).database is None def test_can_repeatedly_push_the_same_thing(): s = settings(max_examples=12) t = settings(max_examples=17) original = settings().max_examples with local_settings(s): assert settings().max_examples == 12 with local_settings(t): assert settings().max_examples == 17 with local_settings(s): assert settings().max_examples == 12 with local_settings(t): assert settings().max_examples == 17 assert settings().max_examples == 12 assert settings().max_examples == 17 assert settings().max_examples == 12 assert settings().max_examples == original def test_can_set_verbosity(): settings(verbosity=Verbosity.quiet) settings(verbosity=Verbosity.normal) settings(verbosity=Verbosity.verbose) settings(verbosity=Verbosity.debug) def test_can_not_set_verbosity_to_non_verbosity(): with pytest.raises(InvalidArgument): settings(verbosity="kittens") @pytest.mark.parametrize("db", [None, InMemoryExampleDatabase()]) def test_inherits_an_empty_database(db): with local_settings(settings(database=InMemoryExampleDatabase())): assert settings.default.database is not None s = settings(database=db) assert s.database is db with local_settings(s): t = settings() assert t.database is db @pytest.mark.parametrize("db", [None, InMemoryExampleDatabase()]) def test_can_assign_database(db): x = settings(database=db) assert x.database is db def test_will_reload_profile_when_default_is_absent(): original = settings.default default_variable.value = None assert settings.default is original def test_load_profile(): with restore_profile(): settings.load_profile("default") assert settings.default.max_examples == original_default assert settings.default.stateful_step_count == 50 settings.register_profile( "test", settings(max_examples=10), stateful_step_count=5 ) settings.load_profile("test") assert settings.default.max_examples == 10 assert settings.default.stateful_step_count == 5 settings.load_profile("default") assert settings.default.max_examples == original_default assert settings.default.stateful_step_count == 50 def test_profile_names_must_be_strings(): with pytest.raises(InvalidArgument): settings.register_profile(5) with pytest.raises(InvalidArgument): settings.get_profile(5) with pytest.raises(InvalidArgument): settings.load_profile(5) def test_loading_profile_keeps_expected_behaviour(): with restore_profile(): settings.register_profile("ci", settings(max_examples=10000)) settings.load_profile("ci") assert settings().max_examples == 10000 with local_settings(settings(max_examples=5)): assert settings().max_examples == 5 assert settings().max_examples == 10000 def test_load_non_existent_profile(): with pytest.raises(InvalidArgument): settings.get_profile("nonsense") def test_cannot_set_settings(): x = settings() with pytest.raises(AttributeError): x.max_examples = "foo" with pytest.raises(AttributeError): x.database = "foo" assert x.max_examples != "foo" assert x.database != "foo" def test_can_have_none_database(): assert settings(database=None).database is None @pytest.mark.parametrize("db", [None, InMemoryExampleDatabase()]) @pytest.mark.parametrize("bad_db", [":memory:", ".hypothesis/examples"]) def test_database_type_must_be_ExampleDatabase(db, bad_db): with local_settings(settings(database=db)): settings_property_db = settings.database with pytest.raises(InvalidArgument): settings(database=bad_db) assert settings.database is settings_property_db def test_cannot_assign_default(): with pytest.raises(AttributeError): settings.default = settings(max_examples=3) assert settings().max_examples != 3 @settings(max_examples=7) @given(st.builds(lambda: settings.default)) def test_settings_in_strategies_are_from_test_scope(s): assert s.max_examples == 7 TEST_SETTINGS_ALONE = """ from hypothesis import settings from hypothesis.strategies import integers @settings() def test_settings_alone(): pass """ # runpytest_inprocess uses invalidate_caches in pytest, which is not thread safe # (I presume; produces keyerrors). @skipif_threading def test_settings_alone(pytester): # Disable cacheprovider, since we don't need it and it's flaky on pyodide script = pytester.makepyfile(TEST_SETTINGS_ALONE) result = pytester.runpytest_inprocess(script, "-p", "no:cacheprovider") out = "\n".join(result.stdout.lines) msg = "Using `@settings` on a test without `@given` is completely pointless." assert msg in out assert "InvalidArgument" in out assert result.ret == 1 @fails_with(InvalidArgument) def test_settings_applied_twice_is_error(): @given(st.integers()) @settings() @settings() def test_nothing(x): pass @settings() @given(st.integers()) def test_outer_ok(x): pass @given(st.integers()) @settings() def test_inner_ok(x): pass def test_settings_as_decorator_must_be_on_callable(): with pytest.raises(InvalidArgument): settings()(1) ASSERT_DATABASE_PATH = """ import tempfile from hypothesis import settings from hypothesis.configuration import set_hypothesis_home_dir from hypothesis.database import DirectoryBasedExampleDatabase settings.load_profile("default") settings.default.database if __name__ == '__main__': new_home = tempfile.mkdtemp() set_hypothesis_home_dir(new_home) db = settings.default.database assert isinstance(db, DirectoryBasedExampleDatabase), db assert db.path.is_relative_to(new_home), (db.path, new_home) """ @skipif_emscripten def test_puts_the_database_in_the_home_dir_by_default(tmp_path): script = tmp_path / "assertlocation.py" script.write_text(ASSERT_DATABASE_PATH, encoding="utf-8") subprocess.check_call([sys.executable, str(script)]) def test_database_is_reference_preserved(): s = settings(database=not_set) assert s.database is s.database @settings(verbosity=Verbosity.verbose) @example(x=99) @given(st.integers()) def test_settings_apply_for_explicit_examples(x): # Regression test for #1521 assert settings.default.verbosity == Verbosity.verbose class TestGivenExampleSettingsExplicitCalled(TestCase): """Real nasty edge case here. in #2160, if ``example`` is after ``given`` but before ``settings``, it will be completely ignored. If we set phases to only ``explicit``, the test case will never be called! We have to run an assertion outside of the test case itself. """ @counts_calls def call_target(self): pass @given(st.booleans()) @example(True) @settings(phases=[Phase.explicit]) # counts_calls is not thread safe (modifying global f.calls attr) @skipif_threading def test_example_explicit(self, x): self.call_target() def tearDown(self): # In #2160, this is 0. assert self.call_target.calls == 1 def test_setattr_on_settings_singleton_is_error(): # https://github.com/pandas-dev/pandas/pull/22679#issuecomment-420750921 # Should be setting attributes on settings.default, not settings! with pytest.raises(AttributeError): settings.max_examples = 10 def test_deadline_given_none(): x = settings(deadline=None).deadline assert x is None def test_deadline_given_valid_int(): x = settings(deadline=1000).deadline assert isinstance(x, datetime.timedelta) assert x.days == 0 assert x.seconds == 1 assert x.microseconds == 0 def test_deadline_given_valid_float(): x = settings(deadline=2050.25).deadline assert isinstance(x, datetime.timedelta) assert x.days == 0 assert x.seconds == 2 assert x.microseconds == 50250 def test_deadline_given_valid_timedelta(): x = settings(deadline=datetime.timedelta(days=1, microseconds=15030000)).deadline assert isinstance(x, datetime.timedelta) assert x.days == 1 assert x.seconds == 15 assert x.microseconds == 30000 @pytest.mark.parametrize("value", ["always"]) def test_can_not_set_print_blob_to_non_print_settings(value): with pytest.raises(InvalidArgument): settings(print_blob=value) settings_step_count = 1 @settings(stateful_step_count=settings_step_count) class StepCounter(RuleBasedStateMachine): def __init__(self): super().__init__() self.step_count = 0 @rule() def count_step(self): self.step_count += 1 def teardown(self): assert self.step_count <= settings_step_count test_settings_decorator_applies_to_rule_based_state_machine_class = StepCounter.TestCase def test_two_settings_decorators_applied_to_state_machine_class_raises_error(): with pytest.raises(InvalidArgument): @settings() @settings() class StatefulTest(RuleBasedStateMachine): pass def test_settings_decorator_applied_to_non_state_machine_class_raises_error(): with pytest.raises(InvalidArgument): @settings() class NonStateMachine: pass def test_assigning_to_settings_attribute_on_state_machine_raises_error(): class StateMachine(RuleBasedStateMachine): @rule(x=st.none()) def a_rule(self, x): assert x is None with pytest.raises(AttributeError): StateMachine.settings = settings() state_machine_instance = StateMachine() state_machine_instance.settings = "any value" def test_derandomise_with_explicit_database_is_invalid(): with pytest.raises(InvalidArgument): settings(derandomize=True, database=InMemoryExampleDatabase()) @pytest.mark.parametrize( "kwargs", [ {"max_examples": -1}, {"max_examples": 2.5}, {"stateful_step_count": -1}, {"stateful_step_count": 2.5}, {"deadline": -1}, {"deadline": 0}, {"deadline": -0.7}, {"deadline": 86400000000000000.2}, {"deadline": datetime.timedelta(microseconds=-1)}, {"deadline": datetime.timedelta(0)}, {"deadline": True}, {"deadline": False}, {"backend": "nonexistent_backend"}, {"suppress_health_check": ["nonexistent_healthcheck"]}, {"phases": ["nonexistent_phase"]}, {"phases": 0}, {"verbosity": -1}, {"verbosity": "nonexistent_verbosity"}, ], ) def test_invalid_settings_are_errors(kwargs): with pytest.raises(InvalidArgument): settings(**kwargs) def test_invalid_parent(): class NotSettings: def __repr__(self): return "(not settings repr)" not_settings = NotSettings() with pytest.raises(InvalidArgument, match=r"parent=\(not settings repr\)"): settings(not_settings) def test_default_settings_do_not_use_ci(): assert settings.get_profile("default").suppress_health_check == () def test_show_changed(): s = settings(settings.get_profile("default"), max_examples=999, database=None) assert s.show_changed() == "database=None, max_examples=999" def test_note_deprecation_checks_date(): with pytest.warns(HypothesisDeprecationWarning) as rec: note_deprecation("This is bad", since="RELEASEDAY", has_codemod=False) assert len(rec) == 1 with pytest.raises(AssertionError): note_deprecation("This is way too old", since="1999-12-31", has_codemod=False) def test_note_deprecation_checks_has_codemod(): with pytest.warns( HypothesisDeprecationWarning, match="The `hypothesis codemod` command-line tool", ): note_deprecation("This is bad", since="2021-01-01", has_codemod=True) def test_deprecated_settings_warn_on_set_settings(): with validate_deprecation(): settings(suppress_health_check=[HealthCheck.return_value]) with validate_deprecation(): settings(suppress_health_check=[HealthCheck.not_a_test_method]) @checks_deprecated_behaviour def test_deprecated_settings_not_in_settings_all_list(): al = HealthCheck.all() ls = list(HealthCheck) assert al == ls assert HealthCheck.return_value not in ls assert HealthCheck.not_a_test_method not in ls @skipif_emscripten def test_check_defaults_to_derandomize_when_running_on_ci(): env = dict(os.environ) env["CI"] = "true" assert ( subprocess.check_output( [ sys.executable, "-c", "from hypothesis import settings\nprint(settings().derandomize)", ], env=env, text=True, encoding="utf-8", ).strip() == "True" ) @skipif_emscripten def test_check_defaults_to_randomize_when_not_running_on_ci(): env = dict(os.environ) for key in _CI_VARS: env.pop(key, None) assert ( subprocess.check_output( [ sys.executable, "-c", "from hypothesis import settings\nprint(settings().derandomize)", ], env=env, text=True, encoding="utf-8", ).strip() == "False" ) @skipif_threading # modifying global state (profiles) during testing def test_reloads_the_loaded_profile_if_registered_again(): with restore_profile(): test_profile = "some nonsense profile purely for this test" test_value = 123456 settings.register_profile(test_profile, settings(max_examples=test_value)) settings.load_profile(test_profile) assert settings.default.max_examples == test_value test_value_2 = 42 settings.register_profile(test_profile, settings(max_examples=test_value_2)) assert settings.default.max_examples == test_value_2 CI_TESTING_SCRIPT = """ from hypothesis import settings if __name__ == '__main__': settings.register_profile("ci", settings(max_examples=42)) assert settings.default.max_examples == 42 """ @skipif_emscripten def test_will_automatically_pick_up_changes_to_ci_profile_in_ci(): env = dict(os.environ) env["CI"] = "true" subprocess.check_call( [sys.executable, "-c", CI_TESTING_SCRIPT], env=env, text=True, encoding="utf-8", ) def test_register_profile_avoids_intermediate_profiles(): parent = settings() s = settings(parent, max_examples=10) with temp_register_profile("for_intermediate_test", s): assert settings.get_profile("for_intermediate_test")._fallback is parent @checks_deprecated_behaviour @settings(max_examples=10) @given(st.integers()) def test_cannot_register_profile_from_inside_test(x): settings.register_profile("problematic", settings(max_examples=20)) def test_can_set_verbosity_to_strings(): assert settings(verbosity="quiet").verbosity is Verbosity.quiet assert settings(verbosity="normal").verbosity is Verbosity.normal assert settings(verbosity="verbose").verbosity is Verbosity.verbose assert settings(verbosity="debug").verbosity is Verbosity.debug def test_can_set_phase_to_strings(): assert settings(phases=["reuse"]).phases == (Phase.reuse,) assert settings(phases=["reuse", "explicit"]).phases == ( Phase.explicit, Phase.reuse, ) def test_can_set_suppressions_to_strings(): assert settings( suppress_health_check=["filter_too_much"] ).suppress_health_check == (HealthCheck.filter_too_much,) assert settings( suppress_health_check=["filter_too_much", "too_slow"] ).suppress_health_check == (HealthCheck.filter_too_much, HealthCheck.too_slow) def test_verbosity_is_comparable(): assert Verbosity.quiet < Verbosity.normal assert Verbosity.quiet <= Verbosity.quiet assert Verbosity.quiet == Verbosity.quiet assert Verbosity.quiet >= Verbosity.quiet assert Verbosity.debug > Verbosity.quiet # make sure we're comparing by int value, not by str value assert Verbosity.quiet < Verbosity.normal < Verbosity.verbose < Verbosity.debug # also comparable with other ints assert Verbosity.quiet < 1 assert Verbosity.quiet <= 1 assert Verbosity.quiet == 0 assert Verbosity.quiet >= 0 assert Verbosity.normal > 0 @checks_deprecated_behaviour def test_can_set_verbosity_to_integers(): assert Verbosity(0) is Verbosity.quiet assert Verbosity(1) is Verbosity.normal assert Verbosity(2) is Verbosity.verbose assert Verbosity(3) is Verbosity.debug @checks_deprecated_behaviour def test_can_set_phase_to_integers(): assert Phase(0) is Phase.explicit assert Phase(1) is Phase.reuse assert Phase(2) is Phase.generate assert Phase(4) is Phase.shrink @checks_deprecated_behaviour def test_can_set_suppressions_to_integers(): assert HealthCheck(1) is HealthCheck.data_too_large assert HealthCheck(2) is HealthCheck.filter_too_much assert HealthCheck(3) is HealthCheck.too_slow def test_invalid_integer_phase_raises(): with pytest.raises(ValueError): Phase(99) def test_invalid_integer_healthcheck_raises(): with pytest.raises(ValueError): HealthCheck(99) ================================================ FILE: hypothesis-python/tests/cover/test_setup_teardown.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import HealthCheck, assume, given, settings from hypothesis.strategies import integers, text class HasSetup: def setup_example(self): self.setups = getattr(self, "setups", 0) self.setups += 1 class HasTeardown: def teardown_example(self, ex): self.teardowns = getattr(self, "teardowns", 0) self.teardowns += 1 class SomeGivens: @given(integers()) @settings(suppress_health_check=[HealthCheck.differing_executors]) def give_me_an_int(self, x): pass @given(text()) def give_me_a_string(self, x): pass @given(integers()) @settings(max_examples=1000) def give_me_a_positive_int(self, x): assert x >= 0 @given(integers().map(lambda x: x.nope)) def fail_in_reify(self, x): pass @given(integers()) @settings(suppress_health_check=[HealthCheck.filter_too_much]) def assume_some_stuff(self, x): assume(x > 0) @given(integers().filter(lambda x: x > 0)) def assume_in_reify(self, x): pass class HasSetupAndTeardown(HasSetup, HasTeardown, SomeGivens): pass def test_calls_setup_and_teardown_on_self_as_first_argument(): x = HasSetupAndTeardown() x.give_me_an_int() x.give_me_a_string() assert x.setups > 0 assert x.teardowns == x.setups def test_calls_setup_and_teardown_on_self_unbound(): x = HasSetupAndTeardown() HasSetupAndTeardown.give_me_an_int(x) assert x.setups > 0 assert x.teardowns == x.setups def test_calls_setup_and_teardown_on_failure(): x = HasSetupAndTeardown() with pytest.raises(AssertionError): x.give_me_a_positive_int() assert x.setups > 0 assert x.teardowns == x.setups def test_still_tears_down_on_error_in_generation(): x = HasSetupAndTeardown() with pytest.raises(AttributeError): x.fail_in_reify() assert x.setups > 0 assert x.teardowns == x.setups def test_still_tears_down_on_failed_assume(): x = HasSetupAndTeardown() x.assume_some_stuff() assert x.setups > 0 assert x.teardowns == x.setups def test_still_tears_down_on_failed_assume_in_reify(): x = HasSetupAndTeardown() x.assume_in_reify() assert x.setups > 0 assert x.teardowns == x.setups def test_sets_up_without_teardown(): class Foo(HasSetup, SomeGivens): pass x = Foo() x.give_me_an_int() assert x.setups > 0 assert not hasattr(x, "teardowns") def test_tears_down_without_setup(): class Foo(HasTeardown, SomeGivens): pass x = Foo() x.give_me_an_int() assert x.teardowns > 0 assert not hasattr(x, "setups") ================================================ FILE: hypothesis-python/tests/cover/test_shrink_budgeting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys import pytest from hypothesis.internal.conjecture.shrinking import Integer, Ordering @pytest.mark.parametrize( "Shrinker, value", [ (Integer, 2**16), (Integer, int(sys.float_info.max)), (Ordering, [(100,) * 10]), (Ordering, [i * 100 for i in (range(5))]), (Ordering, [i * 100 for i in reversed(range(5))]), ], ) def test_meets_budgetary_requirements(Shrinker, value): shrinker = Shrinker(value, lambda x: x == value) shrinker.run() assert shrinker.calls <= 10 ================================================ FILE: hypothesis-python/tests/cover/test_sideeffect_warnings.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import _hypothesis_globals import pytest from hypothesis import configuration as fs, strategies as st from hypothesis.errors import HypothesisSideeffectWarning IN_INITIALIZATION_ATTR = "in_initialization" # These tests use the pytest plugin enabling infrastructure to restart the side-effect warnings, # rather than trying to induce side-effects during import (and entrypoint loading) itself, which is # hard to do. Manual verification of behaviour during initial import can be done by just injecting # one of the side-effect-inducing statements below directly into hypothesis.entry_points.run(). # Manual validation can also be done by inspecting the relevant state during import and verify that # it is the same as tested here # (_hypothesis_globals.in_initialization > 0, hypothesis.configuration._first_postinit_what is None) @pytest.fixture def _extend_initialization(monkeypatch): assert getattr(_hypothesis_globals, IN_INITIALIZATION_ATTR) <= 0 monkeypatch.setattr(_hypothesis_globals, IN_INITIALIZATION_ATTR, 1) fs.notice_initialization_restarted(warn=False) assert fs._first_postinit_what is None # validates state as given in comment above @pytest.mark.parametrize( "sideeffect, warning_text", [ # the inner lambda ensures the lazy strategy can't be cached (lambda: st.builds(lambda: None).wrapped_strategy, "lazy evaluation"), (lambda: st.deferred(st.none).wrapped_strategy, "deferred evaluation"), (fs.storage_directory, "accessing storage"), ], ) def test_sideeffect_warning(sideeffect, warning_text, _extend_initialization): with pytest.warns(HypothesisSideeffectWarning, match=warning_text): sideeffect() def test_sideeffect_delayed_warning(monkeypatch, _extend_initialization): what = "synthetic side-effect" # _extend_initialization ensures we start at known clean slate (no delayed warnings). # Then: stop initialization, check a side-effect operation, and restart it. monkeypatch.setattr(_hypothesis_globals, IN_INITIALIZATION_ATTR, 0) fs.check_sideeffect_during_initialization(what) fs.check_sideeffect_during_initialization("ignored since not first") # The warning should identify itself as happening after import but before plugin load with pytest.warns(HypothesisSideeffectWarning, match=what + ".*between import"): monkeypatch.setattr(_hypothesis_globals, IN_INITIALIZATION_ATTR, 1) fs.notice_initialization_restarted() ================================================ FILE: hypothesis-python/tests/cover/test_simple_characters.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import unicodedata import pytest from hypothesis.errors import InvalidArgument from hypothesis.strategies import characters from tests.common.debug import ( assert_no_examples, check_can_generate_examples, find_any, minimal, ) from tests.common.utils import fails_with @fails_with(InvalidArgument) def test_nonexistent_category_argument(): check_can_generate_examples(characters(exclude_categories=["foo"])) def test_bad_codepoint_arguments(): with pytest.raises(InvalidArgument): check_can_generate_examples(characters(min_codepoint=42, max_codepoint=24)) def test_exclude_all_available_range(): with pytest.raises(InvalidArgument): check_can_generate_examples( characters( min_codepoint=ord("0"), max_codepoint=ord("0"), exclude_characters="0" ) ) def test_when_nothing_could_be_produced(): with pytest.raises(InvalidArgument): check_can_generate_examples( characters( categories=["Cc"], min_codepoint=ord("0"), max_codepoint=ord("9") ) ) def test_include_exclude_with_multiple_chars_is_invalid(): with pytest.raises(InvalidArgument, match="required to be a single character"): check_can_generate_examples( characters(codec="ascii", exclude_characters=["a", "morethanonechar"]) ) with pytest.raises(InvalidArgument, match="required to be a single character"): check_can_generate_examples( characters(codec="ascii", include_characters=["a", "morethanonechar"]) ) def test_characters_of_specific_groups(): st = characters(categories=("Lu", "Nd")) find_any(st, lambda c: unicodedata.category(c) == "Lu") find_any(st, lambda c: unicodedata.category(c) == "Nd") assert_no_examples(st, lambda c: unicodedata.category(c) not in ("Lu", "Nd")) def test_characters_of_major_categories(): st = characters(categories=("L", "N")) find_any(st, lambda c: unicodedata.category(c).startswith("L")) find_any(st, lambda c: unicodedata.category(c).startswith("N")) assert_no_examples(st, lambda c: unicodedata.category(c)[0] not in ("L", "N")) def test_exclude_characters_of_specific_groups(): st = characters(exclude_categories=("Lu", "Nd")) find_any(st, lambda c: unicodedata.category(c) != "Lu") find_any(st, lambda c: unicodedata.category(c) != "Nd") assert_no_examples(st, lambda c: unicodedata.category(c) in ("Lu", "Nd")) def test_exclude_characters_of_major_categories(): st = characters(exclude_categories=("L", "N")) find_any(st, lambda c: not unicodedata.category(c).startswith("L")) find_any(st, lambda c: not unicodedata.category(c).startswith("N")) assert_no_examples(st, lambda c: unicodedata.category(c)[0] in ("L", "N")) def test_find_one(): char = minimal(characters(min_codepoint=48, max_codepoint=48)) assert char == "0" def test_find_something_rare(): st = characters(categories=["Zs"], min_codepoint=12288) find_any(st, lambda c: unicodedata.category(c) == "Zs") assert_no_examples(st, lambda c: unicodedata.category(c) != "Zs") def test_whitelisted_characters_alone(): with pytest.raises(InvalidArgument): check_can_generate_examples(characters(include_characters="te02тест49st")) def test_whitelisted_characters_overlap_blacklisted_characters(): good_chars = "te02тест49st" bad_chars = "ts94тсет" with pytest.raises(InvalidArgument) as exc: check_can_generate_examples( characters( min_codepoint=ord("0"), max_codepoint=ord("9"), include_characters=good_chars, exclude_characters=bad_chars, ) ) assert repr(good_chars) in str(exc) assert repr(bad_chars) in str(exc) def test_whitelisted_characters_override(): good_characters = "teтестst" st = characters( min_codepoint=ord("0"), max_codepoint=ord("9"), include_characters=good_characters, ) find_any(st, lambda c: c in good_characters) find_any(st, lambda c: c in "0123456789") assert_no_examples(st, lambda c: c not in good_characters + "0123456789") def test_blacklisted_characters(): bad_chars = "te02тест49st" st = characters( min_codepoint=ord("0"), max_codepoint=ord("9"), exclude_characters=bad_chars ) assert "1" == minimal(st) assert_no_examples(st, lambda c: c in bad_chars) def test_whitelist_characters_disjoint_blacklist_characters(): good_chars = "123abc" bad_chars = "456def" st = characters( min_codepoint=ord("0"), max_codepoint=ord("9"), exclude_characters=bad_chars, include_characters=good_chars, ) assert_no_examples(st, lambda c: c in bad_chars) ================================================ FILE: hypothesis-python/tests/cover/test_simple_collections.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import OrderedDict from random import Random import pytest from hypothesis import given, settings from hypothesis.strategies import ( booleans, dictionaries, fixed_dictionaries, frozensets, integers, lists, none, nothing, sets, text, tuples, ) from tests.common.debug import assert_simple_property, find_any, minimal from tests.common.utils import flaky @pytest.mark.parametrize( ("col", "strat"), [ ((), tuples()), ([], lists(none(), max_size=0)), (set(), sets(none(), max_size=0)), (frozenset(), frozensets(none(), max_size=0)), ({}, fixed_dictionaries({})), ({}, fixed_dictionaries({}, optional={})), (OrderedDict(), fixed_dictionaries(OrderedDict(), optional=OrderedDict())), ({}, fixed_dictionaries({}, optional={1: booleans()})), ({0: False}, fixed_dictionaries({0: booleans()}, optional={1: booleans()})), ({}, fixed_dictionaries({}, optional={(): booleans(), 0: booleans()})), ([], lists(nothing())), ([], lists(nothing(), unique=True)), ], ) def test_find_empty_collection_gives_empty(col, strat): assert minimal(strat) == col @pytest.mark.parametrize( ("coltype", "strat"), [(list, lists), (set, sets), (frozenset, frozensets)] ) def test_find_non_empty_collection_gives_single_zero(coltype, strat): assert minimal(strat(integers()), bool) == coltype((0,)) @pytest.mark.parametrize( ("coltype", "strat"), [(list, lists), (set, sets), (frozenset, frozensets)] ) def test_minimizes_to_empty(coltype, strat): assert minimal(strat(integers())) == coltype() def test_minimizes_list_of_lists(): xs = minimal(lists(lists(booleans())), lambda x: any(x) and not all(x)) xs.sort() assert xs == [[], [False]] @given(sets(integers(0, 100), min_size=2, max_size=10)) @settings(max_examples=100) def test_sets_are_size_bounded(xs): assert 2 <= len(xs) <= 10 def test_ordered_dictionaries_preserve_keys(): r = Random() keys = list(range(100)) r.shuffle(keys) assert_simple_property( fixed_dictionaries(OrderedDict([(k, booleans()) for k in keys])), lambda x: list(x.keys()) == keys, ) @given(fixed_dictionaries({}, optional={0: booleans(), 1: nothing(), 2: booleans()})) def test_fixed_dictionaries_with_optional_and_empty_keys(d): assert 1 not in d @pytest.mark.parametrize("n", range(10)) def test_lists_of_fixed_length(n): assert minimal(lists(integers(), min_size=n, max_size=n)) == [0] * n @pytest.mark.parametrize("n", range(10)) def test_sets_of_fixed_length(n): x = minimal(sets(integers(), min_size=n, max_size=n)) assert len(x) == n if n == 0: assert x == set() else: assert x == set(range(min(x), min(x) + n)) @pytest.mark.parametrize("n", range(10)) def test_dictionaries_of_fixed_length(n): x = set(minimal(dictionaries(integers(), booleans(), min_size=n, max_size=n))) if not n: assert x == set() else: assert x == set(range(min(x), min(x) + n)) @pytest.mark.parametrize("n", range(10)) def test_lists_of_lower_bounded_length(n): l = minimal(lists(integers(), min_size=n), lambda x: sum(x) >= 2 * n) assert l == [] if n == 0 else [0] * (n - 1) + [n * 2] @flaky(min_passes=1, max_runs=3) def test_can_find_unique_lists_of_non_set_order(): # This test checks that our strategy for unique lists doesn't accidentally # depend on the iteration order of sets. # # Unfortunately, that means that *this* test has to rely on set iteration # order. That makes it tricky to debug on CPython, because set iteration # order changes every time the process is launched. # # To get around this, define the PYTHONHASHSEED environment variable to # a consistent value. This could be 0, or it could be the PYTHONHASHSEED # value listed in a failure log from CI. ls = minimal( lists(text(), min_size=2, unique=True), lambda x: list(set(reversed(x))) != x, # noqa: C414 # yes, reverse inside set ) assert len(set(ls)) == len(ls) assert len(ls) == 2 def test_can_draw_empty_list_from_unsatisfiable_strategy(): assert find_any(lists(integers().filter(lambda s: False))) == [] def test_can_draw_empty_set_from_unsatisfiable_strategy(): assert find_any(sets(integers().filter(lambda s: False))) == set() @given(lists(sets(none()), min_size=10)) def test_small_sized_sets(x): pass def test_minimize_dicts_with_incompatible_keys(): strat = fixed_dictionaries({1: booleans(), "hi": lists(booleans())}) assert minimal(strat) == {1: False, "hi": []} @given( lists( tuples(integers(), integers()), min_size=2, unique_by=(lambda x: x[0], lambda x: x[1]), ) ) def test_lists_unique_by_tuple_funcs(ls): firstitems, seconditems = zip(*ls, strict=True) assert len(set(firstitems)) == len(firstitems) assert len(set(seconditems)) == len(seconditems) ================================================ FILE: hypothesis-python/tests/cover/test_simple_strings.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given from hypothesis.strategies import binary, characters, text, tuples from tests.common.debug import minimal def test_can_minimize_up_to_zero(): s = minimal(text(), lambda x: any(t <= "0" for t in x)) assert s == "0" def test_minimizes_towards_ascii_zero(): s = minimal(text(), lambda x: any(t < "0" for t in x)) assert s == chr(ord("0") - 1) def test_can_handle_large_codepoints(): s = minimal(text(), lambda x: x >= "☃") assert s == "☃" def test_can_find_mixed_ascii_and_non_ascii_strings(): s = minimal( text(), lambda x: (any(t >= "☃" for t in x) and any(ord(t) <= 127 for t in x)) ) assert len(s) == 2 assert sorted(s) == ["0", "☃"] def test_will_find_ascii_examples_given_the_chance(): s = minimal( tuples(text(max_size=1), text(max_size=1)), lambda x: x[0] and (x[0] < x[1]) ) assert ord(s[1]) == ord(s[0]) + 1 assert "0" in s def test_minimisation_consistent_with_characters(): s = minimal(text("FEDCBA", min_size=3)) assert s == "AAA" def test_finds_single_element_strings(): assert minimal(text(), bool) == "0" @given(binary(max_size=5)) def test_binary_respects_max_size(x): assert len(x) <= 5 def test_does_not_simplify_into_surrogates(): f = minimal(text(), lambda x: x >= "\udfff") assert f == "\ue000" size = 5 f = minimal(text(min_size=size), lambda x: sum(t >= "\udfff" for t in x) >= size) assert f == "\ue000" * size @given(text(alphabet=["a", "b"])) def test_respects_alphabet_if_list(xs): assert set(xs).issubset(set("ab")) @given(text(alphabet="cdef")) def test_respects_alphabet_if_string(xs): assert set(xs).issubset(set("cdef")) @given(text()) def test_can_encode_as_utf8(s): s.encode() @given(text(characters(exclude_characters="\n"))) def test_can_blacklist_newlines(s): assert "\n" not in s @given(text(characters(exclude_categories=("Cc", "Cs")))) def test_can_exclude_newlines_by_category(s): assert "\n" not in s @given(text(characters(max_codepoint=127))) def test_can_restrict_to_ascii_only(s): s.encode("ascii") def test_fixed_size_bytes_just_draw_bytes(): from hypothesis.internal.conjecture.data import ConjectureData x = ConjectureData.for_choices([b"foo"]) assert x.draw(binary(min_size=3, max_size=3)) == b"foo" @given(text(max_size=10**6)) def test_can_set_max_size_large(s): pass ================================================ FILE: hypothesis-python/tests/cover/test_slices.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import HealthCheck, given, settings, strategies as st from tests.common.debug import assert_all_examples, find_any, minimal use_several_sizes = pytest.mark.parametrize("size", [1, 2, 5, 10, 100, 1000]) @use_several_sizes def test_stop_stays_within_bounds(size): assert_all_examples( st.slices(size), lambda x: x.stop is None or (x.stop >= -size and x.stop <= size), ) @use_several_sizes def test_start_stay_within_bounds(size): assert_all_examples( st.slices(size).filter(lambda x: x.start is not None), lambda x: range(size)[x.start] or True, # no IndexError raised ) @use_several_sizes def test_step_stays_within_bounds(size): # indices -> (start, stop, step) # Stop is exclusive so we use -1 as the floor. # This uses the indices that slice produces to make this test more readable # due to how splice processes None being a little complex assert_all_examples( st.slices(size), lambda x: ( x.indices(size)[0] + x.indices(size)[2] <= size and x.indices(size)[0] + x.indices(size)[2] >= -size ) or x.start % size == x.stop % size, ) @use_several_sizes def test_step_will_not_be_zero(size): assert_all_examples(st.slices(size), lambda x: x.step != 0) @use_several_sizes def test_slices_will_shrink(size): sliced = minimal(st.slices(size)) assert sliced.start == 0 or sliced.start is None assert sliced.stop == 0 or sliced.stop is None assert sliced.step is None @given(st.integers(1, 1000)) @settings(deadline=None, suppress_health_check=list(HealthCheck)) def test_step_will_be_negative(size): find_any(st.slices(size), lambda x: (x.step or 1) < 0) @given(st.integers(1, 1000)) @settings(deadline=None) def test_step_will_be_positive(size): find_any(st.slices(size), lambda x: (x.step or 1) > 0) @pytest.mark.parametrize("size", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) def test_stop_will_equal_size(size): find_any(st.slices(size), lambda x: x.stop == size, settings(max_examples=10**6)) @pytest.mark.parametrize("size", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) def test_start_will_equal_size(size): find_any( st.slices(size), lambda x: x.start == size - 1, settings(max_examples=10**6) ) @given(st.integers(1, 1000)) @settings(deadline=None, max_examples=20) def test_start_will_equal_0(size): find_any(st.slices(size), lambda x: x.start == 0) @given(st.integers(1, 1000)) @settings(deadline=None) def test_start_will_equal_stop(size): find_any(st.slices(size), lambda x: x.start == x.stop) def test_size_is_equal_0(): assert_all_examples( st.slices(0), lambda x: x.step != 0 and x.start is None and x.stop is None ) ================================================ FILE: hypothesis-python/tests/cover/test_slippage.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import Phase, assume, given, settings, strategies as st, target from hypothesis.database import InMemoryExampleDatabase from hypothesis.errors import FlakyFailure from hypothesis.internal.compat import ExceptionGroup from hypothesis.internal.conjecture.engine import MIN_TEST_CALLS from tests.common.utils import ( Why, assert_output_contains_failure, capture_out, non_covering_examples, xfail_on_crosshair, ) def capture_reports(test): with capture_out() as o, pytest.raises(ExceptionGroup) as err: test() return o.getvalue() + "\n\n".join( f"{e!r}\n" + "\n".join(getattr(e, "__notes__", [])) for e in (err.value, *err.value.exceptions) ) @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_raises_multiple_failures_with_varying_type(): target = None @settings(database=None, max_examples=100, report_multiple_bugs=True) @given(st.integers()) def test(i): nonlocal target if abs(i) < 1000: return if target is None: # Ensure that we have some space to shrink into, so we can't # trigger an minimal example and mask the other exception type. assume(1003 < abs(i)) target = i exc_class = TypeError if target == i else ValueError raise exc_class output = capture_reports(test) assert "TypeError" in output assert "ValueError" in output @pytest.mark.skipif( settings().backend != "hypothesis", reason="no multiple failures on backends (yet?)" ) def test_shows_target_scores_with_multiple_failures(): @settings(derandomize=True, max_examples=10_000) @given(st.integers()) def test(i): target(i) assert i > 0 assert i < 0 assert "Highest target score:" in capture_reports(test) @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_raises_multiple_failures_when_position_varies(): target = None @settings(max_examples=100, report_multiple_bugs=True) @given(st.integers()) def test(i): nonlocal target if abs(i) < 1000: return if target is None: target = i if target == i: raise ValueError("loc 1") else: raise ValueError("loc 2") output = capture_reports(test) assert "loc 1" in output assert "loc 2" in output @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_replays_both_failing_values(): target = None @settings( database=InMemoryExampleDatabase(), max_examples=500, report_multiple_bugs=True ) @given(st.integers()) def test(i): nonlocal target if abs(i) < 1000: return if target is None: target = i exc_class = TypeError if target == i else ValueError raise exc_class with pytest.raises(ExceptionGroup): test() with pytest.raises(ExceptionGroup): test() @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) @pytest.mark.parametrize("fix", [TypeError, ValueError]) def test_replays_slipped_examples_once_initial_bug_is_fixed(fix): target = [] bug_fixed = False @settings( database=InMemoryExampleDatabase(), max_examples=500, report_multiple_bugs=True ) @given(st.integers()) def test(i): if abs(i) < 1000: return if not target: target.append(i) if i == target[0]: if bug_fixed and fix == TypeError: return raise TypeError if len(target) == 1: target.append(i) if bug_fixed and fix == ValueError: return if i == target[1]: raise ValueError with pytest.raises(ExceptionGroup): test() bug_fixed = True with pytest.raises(ValueError if fix == TypeError else TypeError): test() @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_garbage_collects_the_secondary_key(): target = [] bug_fixed = False db = InMemoryExampleDatabase() @settings(database=db, max_examples=500, report_multiple_bugs=True) @given(st.integers()) def test(i): if bug_fixed: return if abs(i) < 1000: return if not target: target.append(i) if i == target[0]: raise TypeError if len(target) == 1: target.append(i) if i == target[1]: raise ValueError with pytest.raises(ExceptionGroup): test() bug_fixed = True def count(): return len(non_covering_examples(db)) prev = count() while prev > 0: test() current = count() assert current < prev prev = current def test_shrinks_both_failures(): first_has_failed = False duds = set() second_target = None @settings(database=None, max_examples=1000, report_multiple_bugs=True) @given(st.integers(min_value=0)) def test(i): nonlocal first_has_failed, duds, second_target if i >= 10000: first_has_failed = True raise AssertionError assert i < 10000 if first_has_failed: if second_target is None: for j in range(10000): if j not in duds: second_target = j break # to avoid flaky errors, don't error on an input that we previously # passed. if i not in duds: assert i < second_target else: duds.add(i) output = capture_reports(test) assert_output_contains_failure(output, test, i=10000) assert_output_contains_failure(output, test, i=second_target) @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_handles_flaky_tests_where_only_one_is_flaky(): flaky_fixed = False target = [] flaky_failed_once = False @settings( database=InMemoryExampleDatabase(), max_examples=1000, report_multiple_bugs=True ) @given(st.integers()) def test(i): nonlocal flaky_failed_once if abs(i) < 1000: return if not target: target.append(i) if i == target[0]: raise TypeError if flaky_failed_once and not flaky_fixed: return if len(target) == 1: target.append(i) if i == target[1]: flaky_failed_once = True raise ValueError with pytest.raises(ExceptionGroup) as err: test() assert any(isinstance(e, FlakyFailure) for e in err.value.exceptions) flaky_fixed = True with pytest.raises(ExceptionGroup) as err: test() assert not any(isinstance(e, FlakyFailure) for e in err.value.exceptions) @pytest.mark.skipif( settings().backend != "hypothesis", reason="no multiple failures on backends (yet?)" ) @pytest.mark.parametrize("allow_multi", [True, False]) def test_can_disable_multiple_error_reporting(allow_multi): seen = set() @settings(database=None, derandomize=True, report_multiple_bugs=allow_multi) @given(st.integers(min_value=0)) def test(i): # We will pass on the minimal i=0, then fail with a large i, and eventually # slip to i=1 and a different error. We check both seen and raised errors. if i == 1: seen.add(TypeError) raise TypeError elif i >= 2: seen.add(ValueError) raise ValueError with pytest.raises(ExceptionGroup if allow_multi else TypeError): test() assert seen == {TypeError, ValueError} @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_finds_multiple_failures_in_generation(): special = None seen = set() @settings( phases=[Phase.generate, Phase.shrink], max_examples=100, report_multiple_bugs=True, ) @given(st.integers(min_value=0)) def test(x): """Constructs a test so the 10th largeish example we've seen is a special failure, and anything new we see after that point that is larger than it is a different failure. This demonstrates that we can keep generating larger examples and still find new bugs after that point.""" nonlocal special if not special: # don't mark duplicate inputs as special and thus erroring, to avoid # flakiness where we passed the input the first time but failed it the # second. if len(seen) >= 10 and x <= 1000 and x not in seen: special = x else: seen.add(x) if special: assert x in seen or x <= special assert x != special with pytest.raises(ExceptionGroup): test() def test_stops_immediately_if_not_report_multiple_bugs(): seen = set() @settings(phases=[Phase.generate], report_multiple_bugs=False) @given(st.integers()) def test(x): seen.add(x) raise AssertionError with pytest.raises(AssertionError): test() assert len(seen) == 1 @pytest.mark.skipif( settings().backend != "hypothesis", reason="unclear backend semantics" ) def test_stops_immediately_on_replay(): seen = set() @settings(database=InMemoryExampleDatabase(), phases=tuple(Phase)[:-1]) @given(st.integers()) def test(x): seen.add(x) assert x # On the first run, we look for up to ten examples: with pytest.raises(AssertionError): test() assert 1 < len(seen) <= MIN_TEST_CALLS # With failing examples in the database, we stop at one. seen.clear() with pytest.raises(AssertionError): test() assert len(seen) == 1 ================================================ FILE: hypothesis-python/tests/cover/test_stateful.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re from collections import defaultdict from typing import ClassVar import pytest from _pytest.outcomes import Failed, Skipped from pytest import raises from hypothesis import ( HealthCheck, Phase, __version__, reproduce_failure, seed, settings as Settings, strategies as st, ) from hypothesis.control import current_build_context from hypothesis.core import encode_failure from hypothesis.database import InMemoryExampleDatabase from hypothesis.errors import ( DidNotReproduce, Flaky, FlakyStrategyDefinition, InvalidArgument, InvalidDefinition, ) from hypothesis.stateful import ( Bundle, RuleBasedStateMachine, consumes, get_state_machine_test, initialize, invariant, multiple, precondition, rule, run_state_machine_as_test, ) from hypothesis.strategies import binary, data, integers, just, lists from tests.common.utils import ( Why, capture_out, skipif_threading, validate_deprecation, xfail_on_crosshair, ) from tests.nocover.test_stateful import DepthMachine NO_BLOB_SETTINGS = Settings(print_blob=False, phases=tuple(Phase)[:-1]) class MultipleRulesSameFuncMachine(RuleBasedStateMachine): def myfunc(self, data): print(data) rule1 = rule(data=just("rule1data"))(myfunc) rule2 = rule(data=just("rule2data"))(myfunc) class PreconditionMachine(RuleBasedStateMachine): num = 0 @rule() def add_one(self): self.num += 1 @rule() def set_to_zero(self): self.num = 0 @rule(num=integers()) @precondition(lambda self: self.num != 0) def div_by_precondition_after(self, num): self.num = num / self.num @precondition(lambda self: self.num != 0) @rule(num=integers()) def div_by_precondition_before(self, num): self.num = num / self.num TestPrecondition = PreconditionMachine.TestCase TestPrecondition.settings = Settings(TestPrecondition.settings, max_examples=10) def test_picks_up_settings_at_first_use_of_testcase(): assert TestPrecondition.settings.max_examples == 10 def test_multiple_rules_same_func(): test_class = MultipleRulesSameFuncMachine.TestCase with capture_out() as o: test_class().runTest() output = o.getvalue() assert "rule1data" in output assert "rule2data" in output def test_can_get_test_case_off_machine_instance(): assert DepthMachine().TestCase is DepthMachine().TestCase assert DepthMachine().TestCase is not None class FlakyDrawLessMachine(RuleBasedStateMachine): @rule(d=data()) def action(self, d): if current_build_context().is_final: d.draw(binary(min_size=1, max_size=1)) else: buffer = binary(min_size=1024, max_size=1024) assert 0 not in buffer def test_flaky_draw_less_raises_flaky(): with raises(Flaky): FlakyDrawLessMachine.TestCase().runTest() def test_result_is_added_to_target(): class TargetStateMachine(RuleBasedStateMachine): nodes = Bundle("nodes") @rule(target=nodes, source=lists(nodes)) def bunch(self, source): assert len(source) == 0 return source test_class = TargetStateMachine.TestCase try: test_class().runTest() raise RuntimeError("Expected an assertion error") except AssertionError as err: notes = err.__notes__ regularized_notes = [re.sub(r"[0-9]+", "i", note) for note in notes] assert "state.bunch(source=[nodes_i])" in regularized_notes class FlakyStateMachine(RuleBasedStateMachine): @rule() def action(self): assert current_build_context().is_final def test_flaky_raises_flaky(): with raises(Flaky): FlakyStateMachine.TestCase().runTest() class FlakyPreconditionMachine(RuleBasedStateMachine): @precondition(lambda self: not current_build_context().is_final) @rule() def action(self): raise AssertionError def test_flaky_precondition_error_message(): with raises(FlakyStrategyDefinition) as exc_info: FlakyPreconditionMachine.TestCase().runTest() assert any("flaky precondition" in note for note in exc_info.value.__notes__) class FlakyDrawInRuleMachine(RuleBasedStateMachine): # Flakiness inside rule execution (via data().draw()) happens AFTER rule selection, # so the "flaky precondition" note should NOT be added. @rule(d=data()) def action(self, d): if current_build_context().is_final: d.draw(st.integers(0, 0)) d.draw(st.integers()) raise AssertionError def test_flaky_draw_in_rule_no_precondition_note(): # When flakiness occurs during rule execution (not rule selection), # the error message should NOT mention flaky preconditions. with raises(FlakyStrategyDefinition) as exc_info: FlakyDrawInRuleMachine.TestCase().runTest() notes = getattr(exc_info.value, "__notes__", []) assert not any("flaky precondition" in note for note in notes) def test_get_state_machine_test_is_importable(): # Regression test: get_state_machine_test is used by HypoFuzz assert callable(get_state_machine_test) class FlakyRatchettingMachine(RuleBasedStateMachine): ratchet = 0 @rule(d=data()) def action(self, d): FlakyRatchettingMachine.ratchet += 1 n = FlakyRatchettingMachine.ratchet d.draw(lists(integers(), min_size=n, max_size=n)) raise AssertionError @Settings( stateful_step_count=10, max_examples=30, suppress_health_check=[HealthCheck.filter_too_much], ) # speed this up class MachineWithConsumingRule(RuleBasedStateMachine): b1 = Bundle("b1") b2 = Bundle("b2") def __init__(self): self.created_counter = 0 self.consumed_counter = 0 super().__init__() @invariant() def bundle_length(self): assert len(self.bundle("b1")) == self.created_counter - self.consumed_counter @rule(target=b1) def populate_b1(self): self.created_counter += 1 return self.created_counter @rule(target=b2, consumed=consumes(b1)) def depopulate_b1(self, consumed): self.consumed_counter += 1 return consumed @rule(consumed=lists(consumes(b1), max_size=3)) def depopulate_b1_multiple(self, consumed): self.consumed_counter += len(consumed) @rule(value1=b1, value2=b2) def check(self, value1, value2): assert value1 != value2 TestMachineWithConsumingRule = MachineWithConsumingRule.TestCase def test_multiple(): none = multiple() some = multiple(1, 2.01, "3", b"4", 5) assert len(none.values) == 0 assert len(some.values) == 5 assert set(some.values) == {1, 2.01, "3", b"4", 5} class MachineUsingMultiple(RuleBasedStateMachine): b = Bundle("b") def __init__(self): self.expected_bundle_length = 0 super().__init__() @invariant() def bundle_length(self): assert len(self.bundle("b")) == self.expected_bundle_length @rule(target=b, items=lists(elements=integers(), max_size=10)) def populate_bundle(self, items): self.expected_bundle_length += len(items) return multiple(*items) @rule(target=b) def do_not_populate(self): return multiple() TestMachineUsingMultiple = MachineUsingMultiple.TestCase def test_multiple_variables_printed(): class ProducesMultiple(RuleBasedStateMachine): b = Bundle("b") @initialize(target=b) def populate_bundle(self): return multiple(1, 2) @rule() def fail_fast(self): raise AssertionError with raises(AssertionError) as err: run_state_machine_as_test(ProducesMultiple) # This is tightly coupled to the output format of the step printing. # The first line is "Falsifying Example:..." the second is creating # the state machine, the third is calling the "initialize" method. assignment_line = err.value.__notes__[2] # 'populate_bundle()' returns 2 values, so should be # expanded to 2 variables. assert assignment_line == "b_0, b_1 = state.populate_bundle()" # Make sure MultipleResult is iterable so the printed code is valid. # See https://github.com/HypothesisWorks/hypothesis/issues/2311 state = ProducesMultiple() _b_0, _b_1 = state.populate_bundle() with raises(AssertionError): state.fail_fast() def test_multiple_variables_printed_single_element(): # https://github.com/HypothesisWorks/hypothesis/issues/3236 class ProducesMultiple(RuleBasedStateMachine): b = Bundle("b") @initialize(target=b) def populate_bundle(self): return multiple(1) @rule(b=b) def fail_fast(self, b): assert b != 1 with raises(AssertionError) as err: run_state_machine_as_test(ProducesMultiple) assignment_line = err.value.__notes__[2] assert assignment_line == "(b_0,) = state.populate_bundle()" state = ProducesMultiple() (v1,) = state.populate_bundle() state.fail_fast((v1,)) # passes if tuple not unpacked with raises(AssertionError): state.fail_fast(v1) def test_no_variables_printed(): class ProducesNoVariables(RuleBasedStateMachine): b = Bundle("b") @initialize(target=b) def populate_bundle(self): return multiple() @rule() def fail_fast(self): raise AssertionError with raises(AssertionError) as err: run_state_machine_as_test(ProducesNoVariables) # This is tightly coupled to the output format of the step printing. # The first line is "Falsifying Example:..." the second is creating # the state machine, the third is calling the "initialize" method. assignment_line = err.value.__notes__[2] # 'populate_bundle()' returns 0 values, so there should be no # variable assignment. assert assignment_line == "state.populate_bundle()" def test_consumes_typecheck(): with pytest.raises(TypeError): consumes(integers()) def test_ratchetting_raises_flaky(): with raises(Flaky): FlakyRatchettingMachine.TestCase().runTest() def test_empty_machine_is_invalid(): class EmptyMachine(RuleBasedStateMachine): pass with raises(InvalidDefinition): EmptyMachine.TestCase().runTest() def test_machine_with_no_terminals_is_invalid(): class NonTerminalMachine(RuleBasedStateMachine): @rule(value=Bundle("hi")) def bye(self, hi): pass with raises(InvalidDefinition): NonTerminalMachine.TestCase().runTest() def test_minimizes_errors_in_teardown(): # temporary debugging to try to narrow down a potential thread-safety issue import threading from hypothesis import Verbosity counter = 0 @Settings(database=None, verbosity=Verbosity.debug) class Foo(RuleBasedStateMachine): @initialize() def init(self): nonlocal counter counter = 0 print(f"[{threading.get_ident()}] init", counter) @rule() def increment(self): nonlocal counter counter += 1 print(f"[{threading.get_ident()}] increment", counter) def teardown(self): nonlocal counter print(f"[{threading.get_ident()}] teardown", counter) assert not counter with raises(AssertionError): run_state_machine_as_test(Foo) assert counter == 1 class RequiresInit(RuleBasedStateMachine): def __init__(self, threshold): super().__init__() self.threshold = threshold @rule(value=integers()) def action(self, value): if value > self.threshold: raise ValueError(f"{value} is too high") def test_can_use_factory_for_tests(): with raises(ValueError): run_state_machine_as_test( lambda: RequiresInit(42), settings=Settings(max_examples=100) ) class FailsEventually(RuleBasedStateMachine): def __init__(self): super().__init__() self.counter = 0 @rule() def increment(self): self.counter += 1 assert self.counter < 10 FailsEventually.TestCase.settings = Settings(stateful_step_count=5) @skipif_threading def test_can_explicitly_pass_settings(): run_state_machine_as_test(FailsEventually) try: FailsEventually.TestCase.settings = Settings( FailsEventually.TestCase.settings, stateful_step_count=15 ) run_state_machine_as_test( FailsEventually, settings=Settings(stateful_step_count=2) ) finally: FailsEventually.TestCase.settings = Settings( FailsEventually.TestCase.settings, stateful_step_count=5 ) def test_settings_argument_is_validated(): with pytest.raises(InvalidArgument): run_state_machine_as_test(FailsEventually, settings=object()) def test_runner_that_checks_factory_produced_a_machine(): with pytest.raises(InvalidArgument): run_state_machine_as_test(object) @skipif_threading def test_settings_attribute_is_validated(): real_settings = FailsEventually.TestCase.settings try: FailsEventually.TestCase.settings = object() with pytest.raises(InvalidArgument): run_state_machine_as_test(FailsEventually) finally: FailsEventually.TestCase.settings = real_settings def test_saves_failing_example_in_database(): db = InMemoryExampleDatabase() ss = Settings( database=db, max_examples=1000, suppress_health_check=list(HealthCheck) ) with raises(AssertionError): run_state_machine_as_test(DepthMachine, settings=ss) assert any(list(db.data.values())) def test_can_run_with_no_db(): with raises(AssertionError): run_state_machine_as_test( DepthMachine, settings=Settings(database=None, max_examples=10_000) ) def test_stateful_double_rule_is_forbidden(recwarn): with pytest.raises(InvalidDefinition): class DoubleRuleMachine(RuleBasedStateMachine): @rule(num=just(1)) @rule(num=just(2)) def whatevs(self, num): pass def test_can_explicitly_call_functions_when_precondition_not_satisfied(): class BadPrecondition(RuleBasedStateMachine): def __init__(self): super().__init__() @precondition(lambda self: False) @rule() def test_blah(self): raise ValueError @rule() def test_foo(self): self.test_blah() with pytest.raises(ValueError): run_state_machine_as_test(BadPrecondition) def test_invariant(): """If an invariant raise an exception, the exception is propagated.""" class Invariant(RuleBasedStateMachine): def __init__(self): super().__init__() @invariant() def test_blah(self): raise ValueError @rule() def do_stuff(self): pass with pytest.raises(ValueError): run_state_machine_as_test(Invariant) def test_no_double_invariant(): """The invariant decorator can't be applied multiple times to a single function.""" with raises(InvalidDefinition): class Invariant(RuleBasedStateMachine): def __init__(self): super().__init__() @invariant() @invariant() def test_blah(self): pass @rule() def do_stuff(self): pass def test_invariant_precondition(): """If an invariant precodition isn't met, the invariant isn't run. The precondition decorator can be applied in any order. """ class Invariant(RuleBasedStateMachine): def __init__(self): super().__init__() @invariant() @precondition(lambda _: False) def an_invariant(self): raise ValueError @precondition(lambda _: False) @invariant() def another_invariant(self): raise ValueError @rule() def do_stuff(self): pass run_state_machine_as_test(Invariant) @pytest.mark.parametrize( "decorators", [ (invariant(), rule()), (rule(), invariant()), (invariant(), initialize()), (initialize(), invariant()), (invariant(), precondition(lambda self: True), rule()), (rule(), precondition(lambda self: True), invariant()), (precondition(lambda self: True), invariant(), rule()), (precondition(lambda self: True), rule(), invariant()), ], ids=lambda x: "-".join(f.__qualname__.split(".")[0] for f in x), ) def test_invariant_and_rule_are_incompatible(decorators): """It's an error to apply @invariant and @rule to the same method.""" def method(self): pass for d in decorators[:-1]: method = d(method) with pytest.raises(InvalidDefinition): decorators[-1](method) def test_invalid_rule_argument(): """Rule kwargs that are not a Strategy are expected to raise an InvalidArgument error.""" with pytest.raises(InvalidArgument): class InvalidRuleMachine(RuleBasedStateMachine): @rule(strategy=object()) def do_stuff(self): pass def test_invalid_initialize_argument(): """Initialize kwargs that are not a Strategy are expected to raise an InvalidArgument error.""" with pytest.raises(InvalidArgument): class InvalidInitialize(RuleBasedStateMachine): @initialize(strategy=object()) def initialize(self): pass def test_multiple_invariants(): """If multiple invariants are present, they all get run.""" class Invariant(RuleBasedStateMachine): def __init__(self): super().__init__() self.first_invariant_ran = False @invariant() def invariant_1(self): self.first_invariant_ran = True @precondition(lambda self: self.first_invariant_ran) @invariant() def invariant_2(self): raise ValueError @rule() def do_stuff(self): pass with pytest.raises(ValueError): run_state_machine_as_test(Invariant) def test_explicit_invariant_call_with_precondition(): """Invariants can be called explicitly even if their precondition is not satisfied.""" class BadPrecondition(RuleBasedStateMachine): def __init__(self): super().__init__() @precondition(lambda self: False) @invariant() def test_blah(self): raise ValueError @rule() def test_foo(self): self.test_blah() with pytest.raises(ValueError): run_state_machine_as_test(BadPrecondition) def test_invariant_checks_initial_state_if_no_initialize_rules(): """Invariants are checked before any rules run.""" class BadPrecondition(RuleBasedStateMachine): def __init__(self): super().__init__() self.num = 0 @invariant() def test_blah(self): if self.num == 0: raise ValueError @rule() def test_foo(self): self.num += 1 with pytest.raises(ValueError): run_state_machine_as_test(BadPrecondition) def test_invariant_failling_present_in_falsifying_example(): @Settings(print_blob=False) class BadInvariant(RuleBasedStateMachine): @initialize() def initialize_1(self): pass @invariant() def invariant_1(self): raise ValueError @rule() def rule_1(self): pass with pytest.raises(ValueError) as err: run_state_machine_as_test(BadInvariant) result = "\n".join(err.value.__notes__) assert ( result == """ Falsifying example: state = BadInvariant() state.initialize_1() state.invariant_1() state.teardown() """.strip() ) def test_invariant_present_in_falsifying_example(): @Settings(print_blob=False, phases=tuple(Phase)[:-1]) class BadRuleWithGoodInvariants(RuleBasedStateMachine): def __init__(self): super().__init__() self.num = 0 @initialize() def initialize_1(self): pass @invariant(check_during_init=True) def invariant_1(self): pass @invariant(check_during_init=False) def invariant_2(self): pass @precondition(lambda self: self.num > 0) @invariant() def invariant_3(self): pass @rule() def rule_1(self): self.num += 1 if self.num == 2: raise ValueError with pytest.raises(ValueError) as err: run_state_machine_as_test(BadRuleWithGoodInvariants) expected = """ Falsifying example: state = BadRuleWithGoodInvariants() state.invariant_1() state.initialize_1() state.invariant_1() state.invariant_2() state.rule_1() state.invariant_1() state.invariant_2() state.invariant_3() state.rule_1() state.teardown() """.strip() result = "\n".join(err.value.__notes__).strip() assert expected == result def test_always_runs_at_least_one_step(): class CountSteps(RuleBasedStateMachine): def __init__(self): super().__init__() self.count = 0 @rule() def do_something(self): self.count += 1 def teardown(self): assert self.count > 0 run_state_machine_as_test(CountSteps) def test_removes_needless_steps(): """Regression test from an example based on tests/nocover/test_database_agreement.py, but without the expensive bits. Comparing two database implementations in which deletion is broken, so as soon as a key/value pair is successfully deleted the test will now fail if you ever check that key. The main interesting feature of this is that it has a lot of opportunities to generate keys and values before it actually fails, but will still fail with very high probability. """ @Settings(derandomize=True, max_examples=1000, deadline=None) class IncorrectDeletion(RuleBasedStateMachine): def __init__(self): super().__init__() self.__saved = defaultdict(set) self.__deleted = defaultdict(set) keys = Bundle("keys") values = Bundle("values") @rule(target=keys, k=binary()) def k(self, k): return k @rule(target=values, v=binary()) def v(self, v): return v @rule(k=keys, v=values) def save(self, k, v): self.__saved[k].add(v) @rule(k=keys, v=values) def delete(self, k, v): if v in self.__saved[k]: self.__deleted[k].add(v) @rule(k=keys) def values_agree(self, k): assert not self.__deleted[k] with pytest.raises(AssertionError) as err: run_state_machine_as_test(IncorrectDeletion) result = "\n".join(err.value.__notes__) assert result.count(" = state.k(") == 1 assert result.count(" = state.v(") == 1 def test_prints_equal_values_with_correct_variable_name(): @Settings(max_examples=100, suppress_health_check=list(HealthCheck)) class MovesBetweenBundles(RuleBasedStateMachine): b1 = Bundle("b1") b2 = Bundle("b2") @rule(target=b1) def create(self): return [] @rule(target=b2, source=b1) def transfer(self, source): return source @rule(source=b2) def fail(self, source): raise AssertionError with pytest.raises(AssertionError) as err: run_state_machine_as_test(MovesBetweenBundles) result = "\n".join(err.value.__notes__) for m in ["create", "transfer", "fail"]: assert result.count("state." + m) == 1 assert "b1_0 = state.create()" in result assert "b2_0 = state.transfer(source=b1_0)" in result assert "state.fail(source=b2_0)" in result def test_initialize_rule(): @Settings(max_examples=1000) class WithInitializeRules(RuleBasedStateMachine): initialized: ClassVar = [] @initialize() def initialize_a(self): self.initialized.append("a") @initialize() def initialize_b(self): self.initialized.append("b") @initialize() def initialize_c(self): self.initialized.append("c") @rule() def fail_fast(self): raise AssertionError with pytest.raises(AssertionError) as err: run_state_machine_as_test(WithInitializeRules) assert set(WithInitializeRules.initialized[-3:]) == {"a", "b", "c"} result = err.value.__notes__[1:] assert result[0] == "state = WithInitializeRules()" # Initialize rules call order is shuffled assert {result[1], result[2], result[3]} == { "state.initialize_a()", "state.initialize_b()", "state.initialize_c()", } assert result[4] == "state.fail_fast()" assert result[5] == "state.teardown()" def test_initialize_rule_populate_bundle(): class WithInitializeBundleRules(RuleBasedStateMachine): a = Bundle("a") @initialize(target=a, dep=just("dep")) def initialize_a(self, dep): return f"a a_0 with ({dep})" @rule(param=a) def fail_fast(self, param): raise AssertionError WithInitializeBundleRules.TestCase.settings = NO_BLOB_SETTINGS with pytest.raises(AssertionError) as err: run_state_machine_as_test(WithInitializeBundleRules) result = "\n".join(err.value.__notes__) assert ( result == """ Falsifying example: state = WithInitializeBundleRules() a_0 = state.initialize_a(dep='dep') state.fail_fast(param=a_0) state.teardown() """.strip() ) def test_initialize_rule_dont_mix_with_precondition(): with pytest.raises( InvalidDefinition, match=( "BadStateMachine\\.initialize has been decorated with both @initialize " "and @precondition" ), ): class BadStateMachine(RuleBasedStateMachine): @precondition(lambda self: True) @initialize() def initialize(self): pass # Also test decorator application in reverse order with pytest.raises( InvalidDefinition, match=( "BadStateMachineReverseOrder\\.initialize has been decorated with both " "@initialize and @precondition" ), ): class BadStateMachineReverseOrder(RuleBasedStateMachine): @initialize() @precondition(lambda self: True) def initialize(self): pass def test_initialize_rule_dont_mix_with_regular_rule(): with pytest.raises( InvalidDefinition, match="BadStateMachine\\.initialize has been decorated with both @rule and @initialize", ): class BadStateMachine(RuleBasedStateMachine): @rule() @initialize() def initialize(self): pass with pytest.raises( InvalidDefinition, match=( "BadStateMachineReverseOrder\\.initialize has been decorated with both " "@rule and @initialize" ), ): class BadStateMachineReverseOrder(RuleBasedStateMachine): @initialize() @rule() def initialize(self): pass def test_initialize_rule_cannot_be_double_applied(): with pytest.raises( InvalidDefinition, match="BadStateMachine\\.initialize has been decorated with @initialize twice", ): class BadStateMachine(RuleBasedStateMachine): @initialize() @initialize() def initialize(self): pass def test_initialize_rule_in_state_machine_with_inheritance(): class ParentStateMachine(RuleBasedStateMachine): initialized: ClassVar = [] @initialize() def initialize_a(self): self.initialized.append("a") class ChildStateMachine(ParentStateMachine): @initialize() def initialize_b(self): self.initialized.append("b") @rule() def fail_fast(self): raise AssertionError with pytest.raises(AssertionError) as err: run_state_machine_as_test(ChildStateMachine) assert set(ChildStateMachine.initialized[-2:]) == {"a", "b"} result = err.value.__notes__[1:] assert result[0] == "state = ChildStateMachine()" # Initialize rules call order is shuffled assert {result[1], result[2]} == {"state.initialize_a()", "state.initialize_b()"} assert result[3] == "state.fail_fast()" assert result[4] == "state.teardown()" def test_can_manually_call_initialize_rule(): class StateMachine(RuleBasedStateMachine): initialize_called_counter = 0 @initialize() def initialize(self): self.initialize_called_counter += 1 @rule() def fail_eventually(self): self.initialize() assert self.initialize_called_counter <= 2 StateMachine.TestCase.settings = NO_BLOB_SETTINGS with pytest.raises(AssertionError) as err: run_state_machine_as_test(StateMachine) result = "\n".join(err.value.__notes__) assert ( result == """ Falsifying example: state = StateMachine() state.initialize() state.fail_eventually() state.fail_eventually() state.teardown() """.strip() ) def test_steps_printed_despite_pytest_fail(): # Test for https://github.com/HypothesisWorks/hypothesis/issues/1372 @Settings(print_blob=False) class RaisesProblem(RuleBasedStateMachine): @rule() def oops(self): pytest.fail("note that this raises a BaseException") with pytest.raises(Failed) as err: run_state_machine_as_test(RaisesProblem) assert ( "\n".join(err.value.__notes__).strip() == """ Falsifying example: state = RaisesProblem() state.oops() state.teardown()""".strip() ) def test_steps_not_printed_with_pytest_skip(capsys): class RaisesProblem(RuleBasedStateMachine): @rule() def skip_whole_test(self): pytest.skip() with pytest.raises(Skipped): run_state_machine_as_test(RaisesProblem) out, _ = capsys.readouterr() assert "state" not in out def test_rule_deprecation_targets_and_target(): k, v = Bundle("k"), Bundle("v") with pytest.raises(InvalidArgument): rule(targets=(k,), target=v) def test_rule_deprecation_bundle_by_name(): Bundle("k") with pytest.raises(InvalidArgument): rule(target="k") def test_rule_non_bundle_target(): with pytest.raises(InvalidArgument): rule(target=integers()) def test_rule_non_bundle_target_oneof(): k, v = Bundle("k"), Bundle("v") pattern = r".+ `one_of(a, b)` or `a | b` .+" with pytest.raises(InvalidArgument, match=pattern): rule(target=k | v) def test_uses_seed(capsys): @seed(0) class TrivialMachine(RuleBasedStateMachine): @rule() def oops(self): raise AssertionError with pytest.raises(AssertionError): run_state_machine_as_test(TrivialMachine) out, _ = capsys.readouterr() assert "@seed" not in out def test_reproduce_failure_works(): @reproduce_failure(__version__, encode_failure([False, 0, True])) class TrivialMachine(RuleBasedStateMachine): @rule() def oops(self): raise AssertionError with pytest.raises(AssertionError): run_state_machine_as_test(TrivialMachine, settings=Settings(print_blob=True)) def test_reproduce_failure_fails_if_no_error(): @reproduce_failure(__version__, encode_failure([False, 0, True])) class TrivialMachine(RuleBasedStateMachine): @rule() def ok(self): pass with pytest.raises(DidNotReproduce): run_state_machine_as_test(TrivialMachine, settings=Settings(print_blob=True)) def test_cannot_have_zero_steps(): with pytest.raises(InvalidArgument): Settings(stateful_step_count=0) def test_arguments_do_not_use_names_of_return_values(): # See https://github.com/HypothesisWorks/hypothesis/issues/2341 class TrickyPrintingMachine(RuleBasedStateMachine): data = Bundle("data") @initialize(target=data, value=integers()) def init_data(self, value): return value @rule(d=data) def mostly_fails(self, d): assert d == 42 with pytest.raises(AssertionError) as err: run_state_machine_as_test(TrickyPrintingMachine) assert "data_0 = state.init_data(value=0)" in err.value.__notes__ assert "data_0 = state.init_data(value=data_0)" not in err.value.__notes__ class TrickyInitMachine(RuleBasedStateMachine): @initialize() def init_a(self): self.a = 0 @rule() def inc(self): self.a += 1 @invariant() def check_a_positive(self): # This will fail if run before the init_a method, but without # @invariant(check_during_init=True) it will only run afterwards. assert self.a >= 0 def test_invariants_are_checked_after_init_steps(): run_state_machine_as_test(TrickyInitMachine) def test_invariants_can_be_checked_during_init_steps(): class UndefinedMachine(TrickyInitMachine): @invariant(check_during_init=True) def check_a_defined(self): # This will fail because `a` is undefined before the init rule. self.a with pytest.raises(AttributeError): run_state_machine_as_test(UndefinedMachine) def test_check_during_init_must_be_boolean(): invariant(check_during_init=False) invariant(check_during_init=True) with pytest.raises(InvalidArgument): invariant(check_during_init="not a bool") def test_deprecated_target_consumes_bundle(): # It would be nicer to raise this error at runtime, but the internals make # this sadly impractical. Most InvalidDefinition errors happen at, well, # definition-time already anyway, so it's not *worse* than the status quo. with validate_deprecation(): rule(target=consumes(Bundle("b"))) @Settings(stateful_step_count=5) class MinStepsMachine(RuleBasedStateMachine): @initialize() def init_a(self): self.a = 0 @rule() def inc(self): self.a += 1 @invariant() def not_too_many_steps(self): assert self.a < 10 def teardown(self): assert self.a >= 2 # Replay overruns after we trigger a crosshair.util.IgnoreAttempt exception for n=3 @xfail_on_crosshair(Why.other) def test_min_steps_argument(): # You must pass a non-negative integer... for n_steps in (-1, "nan", 5.0): with pytest.raises(InvalidArgument): run_state_machine_as_test(MinStepsMachine, _min_steps=n_steps) # and if you do, we'll take at least that many steps run_state_machine_as_test(MinStepsMachine, _min_steps=3) # (oh, and it's OK if you ask for more than we're actually going to take) run_state_machine_as_test(MinStepsMachine, _min_steps=20) class ErrorsOnClassAttributeSettings(RuleBasedStateMachine): settings = Settings(derandomize=True) @rule() def step(self): pass def test_fails_on_settings_class_attribute(): with pytest.raises( InvalidDefinition, match=r"Assigning .+ as a class attribute does nothing", ): run_state_machine_as_test(ErrorsOnClassAttributeSettings) def test_single_target_multiple(): class Machine(RuleBasedStateMachine): a = Bundle("a") @initialize(target=a) def initialize(self): return multiple("ret1", "ret2", "ret3") @rule(param=a) def fail_fast(self, param): raise AssertionError Machine.TestCase.settings = NO_BLOB_SETTINGS with pytest.raises(AssertionError) as err: run_state_machine_as_test(Machine) result = "\n".join(err.value.__notes__) assert ( result == """ Falsifying example: state = Machine() a_0, a_1, a_2 = state.initialize() state.fail_fast(param=a_2) state.teardown() """.strip() ) @pytest.mark.parametrize( "bundle_names,initial,repr_", [ ("a", "ret1", "a_0 = state.init()"), ("aba", "ret1", "a_0 = b_0 = a_1 = state.init()"), ("a", multiple(), "state.init()"), ("aba", multiple(), "state.init()"), ("a", multiple("ret1"), "(a_0,) = state.init()"), ("aba", multiple("ret1"), "(a_0,) = (b_0,) = (a_1,) = state.init()"), ("a", multiple("ret1", "ret2"), "a_0, a_1 = state.init()"), ( "aba", multiple("ret1", "ret2"), "\n".join( # noqa: FLY002 # no, f-string is not more readable [ "a_0, a_1 = state.init()", "b_0, b_1 = a_0, a_1", "a_2, a_3 = a_0, a_1", ] ), ), ], ) def test_targets_repr(bundle_names, initial, repr_): bundles = {name: Bundle(name) for name in bundle_names} class Machine(RuleBasedStateMachine): @initialize(targets=[bundles[name] for name in bundle_names]) def init(self): return initial @rule() def fail_fast(self): raise AssertionError Machine.TestCase.settings = NO_BLOB_SETTINGS with pytest.raises(AssertionError) as err: run_state_machine_as_test(Machine) result = "\n".join(err.value.__notes__) assert ( result == f""" Falsifying example: state = Machine() {repr_} state.fail_fast() state.teardown() """.strip() ) def test_multiple_targets(): class Machine(RuleBasedStateMachine): a = Bundle("a") b = Bundle("b") @initialize(targets=(a, b)) def initialize(self): return multiple("ret1", "ret2", "ret3") @rule( a1=consumes(a), a2=consumes(a), a3=consumes(a), b1=consumes(b), b2=consumes(b), b3=consumes(b), ) def fail_fast(self, a1, a2, a3, b1, b2, b3): raise AssertionError Machine.TestCase.settings = NO_BLOB_SETTINGS with pytest.raises(AssertionError) as err: run_state_machine_as_test(Machine) result = "\n".join(err.value.__notes__) assert ( result == """ Falsifying example: state = Machine() a_0, a_1, a_2 = state.initialize() b_0, b_1, b_2 = a_0, a_1, a_2 state.fail_fast(a1=a_2, a2=a_1, a3=a_0, b1=b_2, b2=b_1, b3=b_0) state.teardown() """.strip() ) def test_multiple_common_targets(): class Machine(RuleBasedStateMachine): a = Bundle("a") b = Bundle("b") @initialize(targets=(a, b, a)) def initialize(self): return multiple("ret1", "ret2", "ret3") @rule( a1=consumes(a), a2=consumes(a), a3=consumes(a), a4=consumes(a), a5=consumes(a), a6=consumes(a), b1=consumes(b), b2=consumes(b), b3=consumes(b), ) def fail_fast(self, a1, a2, a3, a4, a5, a6, b1, b2, b3): raise AssertionError Machine.TestCase.settings = NO_BLOB_SETTINGS with pytest.raises(AssertionError) as err: run_state_machine_as_test(Machine) result = "\n".join(err.value.__notes__) assert ( result == """ Falsifying example: state = Machine() a_0, a_1, a_2 = state.initialize() b_0, b_1, b_2 = a_0, a_1, a_2 a_3, a_4, a_5 = a_0, a_1, a_2 state.fail_fast(a1=a_5, a2=a_4, a3=a_3, a4=a_2, a5=a_1, a6=a_0, b1=b_2, b2=b_1, b3=b_0) state.teardown() """.strip() ) class LotsOfEntropyPerStepMachine(RuleBasedStateMachine): # Regression tests for https://github.com/HypothesisWorks/hypothesis/issues/3618 @rule(data=binary(min_size=512, max_size=512)) def rule1(self, data): assert data @pytest.mark.skipif( Settings.get_current_profile_name() == "crosshair", reason="takes hours; too much symbolic data", ) def test_lots_of_entropy(): run_state_machine_as_test(LotsOfEntropyPerStepMachine) def test_flatmap(): class Machine(RuleBasedStateMachine): buns = Bundle("buns") @initialize(target=buns) def create_bun(self): return 0 @rule(target=buns, bun=buns.flatmap(lambda x: just(x + 1))) def use_flatmap(self, bun): assert isinstance(bun, int) return bun @rule(bun=buns) def use_directly(self, bun): assert isinstance(bun, int) Machine.TestCase.settings = Settings(stateful_step_count=5, max_examples=10) run_state_machine_as_test(Machine) def test_use_bundle_within_other_strategies(): class Class: def __init__(self, value): self.value = value class Machine(RuleBasedStateMachine): my_bundle = Bundle("my_bundle") @initialize(target=my_bundle) def set_initial(self, /) -> str: return "sample text" @rule(instance=st.builds(Class, my_bundle)) def check(self, instance): assert isinstance(instance, Class) assert isinstance(instance.value, str) Machine.TestCase.settings = Settings(stateful_step_count=5, max_examples=10) run_state_machine_as_test(Machine) def test_precondition_cannot_be_used_without_rule(): class BadStateMachine(RuleBasedStateMachine): @precondition(lambda: True) def has_precondition_but_no_rule(self): pass @rule(n=st.integers()) def trivial(self, n): pass with pytest.raises( InvalidDefinition, match=( "BadStateMachine\\.has_precondition_but_no_rule has been decorated " "with @precondition, but not @rule" ), ): BadStateMachine.TestCase().runTest() ================================================ FILE: hypothesis-python/tests/cover/test_statistical_events.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import time import traceback import pytest from hypothesis import ( HealthCheck, assume, event, example, given, reject, settings, stateful, strategies as st, target, ) from hypothesis.control import current_build_context from hypothesis.statistics import collector, describe_statistics from tests.common.utils import Why, xfail_on_crosshair def call_for_statistics(test_function): result = [] with collector.with_value(result.append): try: test_function() except Exception: traceback.print_exc() assert len(result) == 1, result return result[0] def unique_events(stats): return set(sum((t["events"] for t in stats["generate-phase"]["test-cases"]), [])) def test_notes_hard_to_satisfy(): @given(st.integers()) @settings(suppress_health_check=list(HealthCheck)) def test(i): assume(i == 13) stats = call_for_statistics(test) assert "satisfied assumptions" in stats["stopped-because"] def test_can_callback_with_a_string(): @given(st.integers()) def test(i): event("hi") stats = call_for_statistics(test) assert any("hi" in s for s in unique_events(stats)) counter = 0 seen = [] class Foo: def __eq__(self, other): return True def __ne__(self, other): return False def __hash__(self): return 0 def __str__(self): seen.append(self) global counter counter += 1 return f"COUNTER {counter}" def test_formats_are_evaluated_only_once(): global counter counter = 0 @given(st.integers()) def test(i): if current_build_context().data.provider.avoid_realization: pytest.skip("event() cache is disabled under avoid_realization = True") event(Foo()) stats = call_for_statistics(test) assert "COUNTER 1" in unique_events(stats) assert "COUNTER 2" not in unique_events(stats) def test_does_not_report_on_examples(): @example("hi") @given(st.integers()) def test(i): if isinstance(i, str): event("boo") stats = call_for_statistics(test) assert not unique_events(stats) def test_exact_timing(): @settings(suppress_health_check=[HealthCheck.too_slow], deadline=None) @given(st.integers()) def test(i): time.sleep(0.5) stats = describe_statistics(call_for_statistics(test)) assert "~ 500ms" in stats def test_apparently_instantaneous_tests(): time.freeze() @given(st.integers()) def test(i): pass stats = describe_statistics(call_for_statistics(test)) assert "< 1ms" in stats @xfail_on_crosshair(Why.other) # crosshair re-executes for flakiness itself def test_flaky_exit(): first = True @settings(derandomize=True) @given(st.integers()) def test(i): nonlocal first if i > 1001: if first: first = False raise AssertionError stats = call_for_statistics(test) assert stats["stopped-because"] == "test was flaky" @pytest.mark.parametrize("draw_delay", [False, True]) @pytest.mark.parametrize("test_delay", [False, True]) def test_draw_timing(draw_delay, test_delay): time.freeze() @st.composite def s(draw): if draw_delay: time.sleep(0.05) draw(st.integers()) @given(s()) def test(_): if test_delay: time.sleep(0.05) stats = describe_statistics(call_for_statistics(test)) if not draw_delay: assert "< 1ms" in stats else: match = re.search(r"of which ~ (?P\d+)", stats) assert 49 <= int(match.group("gentime")) <= 51 def test_has_lambdas_in_output(): @settings(max_examples=100, database=None) @given(st.integers().filter(lambda x: x % 2 == 0)) def test(i): pass stats = call_for_statistics(test) assert any("lambda x: x % 2 == 0" in e for e in unique_events(stats)) def test_stops_after_x_shrinks(monkeypatch): # the max_shrinks argument is deprecated, but we still stop after some # number - which we can reduce to zero to check that this works. from hypothesis.internal.conjecture import engine monkeypatch.setattr(engine, "MAX_SHRINKS", 0) @given(st.integers(min_value=0)) def test(n): assert n < 10 stats = call_for_statistics(test) assert "shrunk example" in stats["stopped-because"] def test_stateful_states_are_deduped(): class DemoStateMachine(stateful.RuleBasedStateMachine): Stuff = stateful.Bundle("stuff") @stateful.rule(target=Stuff, name=st.text()) def create_stuff(self, name): return name @stateful.rule(item=Stuff) def do(self, item): return stats = call_for_statistics(DemoStateMachine.TestCase().runTest) stats = unique_events(stats) stats = [s for s in stats if not s.startswith("invalid because: (internal)")] assert len(stats) <= 2 def test_stateful_with_one_of_bundles_states_are_deduped(): class DemoStateMachine(stateful.RuleBasedStateMachine): Things = stateful.Bundle("things") Stuff = stateful.Bundle("stuff") StuffAndThings = Things | Stuff @stateful.rule(target=Things, name=st.text()) def create_thing(self, name): return name @stateful.rule(target=Stuff, name=st.text()) def create_stuff(self, name): return name @stateful.rule(item=StuffAndThings) def do(self, item): return stats = call_for_statistics(DemoStateMachine.TestCase().runTest) stats = unique_events(stats) stats = [s for s in stats if not s.startswith("invalid because: (internal)")] assert len(stats) <= 4 def test_statistics_for_threshold_problem(): @settings(max_examples=100, database=None) @given(st.floats(min_value=0, allow_infinity=False)) def threshold(error): target(error, label="error") assert error <= 10 target(0.0, label="never in failing example") stats = call_for_statistics(threshold) assert " - Highest target scores:" in describe_statistics(stats) assert "never in failing example" in describe_statistics(stats) # Check that we report far-from-threshold failing examples assert stats["targets"]["error"] > 10 def test_statistics_with_events_and_target(): @given(st.integers(0, 10_000)) def test(value): event(value) target(float(value), label="a target") stats = describe_statistics(call_for_statistics(test)) assert "- Events:" in stats assert "- Highest target score: " in stats @given(st.booleans()) def test_event_with_non_weakrefable_keys(b): event((b,)) def test_assume_adds_event_with_function_origin(): @given(st.integers()) def very_distinguishable_name(n): assume(n > 100) stats = call_for_statistics(very_distinguishable_name) for tc in stats["generate-phase"]["test-cases"]: for e in tc["events"]: assert "failed to satisfy assume() in very_distinguishable_name" in e def test_reject_adds_event_with_function_origin(): @given(st.integers()) def very_distinguishable_name(n): if n > 100: reject() stats = call_for_statistics(very_distinguishable_name) for tc in stats["generate-phase"]["test-cases"]: for e in tc["events"]: assert "reject() in very_distinguishable_name" in e ================================================ FILE: hypothesis-python/tests/cover/test_subnormal_floats.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from sys import float_info import pytest from hypothesis.errors import InvalidArgument from hypothesis.internal.floats import next_down, next_up from hypothesis.strategies import floats from hypothesis.strategies._internal.numbers import next_down_normal, next_up_normal from tests.common.debug import assert_no_examples, check_can_generate_examples, find_any from tests.common.utils import PYTHON_FTZ pytestmark = [pytest.mark.skipif(PYTHON_FTZ, reason="broken by unsafe compiler flags")] def kw(marks=(), **kwargs): id_ = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) return pytest.param(kwargs, id=id_, marks=marks) @pytest.mark.parametrize( "kwargs", [ kw(min_value=1), kw(max_value=-1), kw(min_value=float_info.min), kw(min_value=next_down(float_info.min), exclude_min=True), kw(max_value=-float_info.min), kw(min_value=next_up(-float_info.min), exclude_max=True), ], ) def test_subnormal_validation(kwargs): strat = floats(**kwargs, allow_subnormal=True) with pytest.raises(InvalidArgument): check_can_generate_examples(strat) @pytest.mark.parametrize( "kwargs", [ # min value kw(allow_subnormal=False, min_value=1), kw(allow_subnormal=False, min_value=float_info.min), kw(allow_subnormal=True, min_value=-1), kw(allow_subnormal=True, min_value=next_down(float_info.min)), # max value kw(allow_subnormal=False, max_value=-1), kw(allow_subnormal=False, max_value=-float_info.min), kw(allow_subnormal=True, max_value=1), kw(allow_subnormal=True, max_value=next_up(-float_info.min)), # min/max values kw(allow_subnormal=True, min_value=-1, max_value=1), kw( allow_subnormal=True, min_value=next_down(float_info.min), max_value=float_info.min, ), kw( allow_subnormal=True, min_value=-float_info.min, max_value=next_up(-float_info.min), ), kw(allow_subnormal=False, min_value=-1, max_value=-float_info.min), kw(allow_subnormal=False, min_value=float_info.min, max_value=1), ], ) def test_allow_subnormal_defaults_correctly(kwargs): # copy to support our threading CI tests kwargs = kwargs.copy() allow_subnormal = kwargs.pop("allow_subnormal") strat = floats(**kwargs).filter(lambda x: x != 0) if allow_subnormal: find_any(strat, lambda x: -float_info.min < x < float_info.min) else: assert_no_examples(strat, lambda x: -float_info.min < x < float_info.min) @pytest.mark.parametrize( "func, val, expected", [ (next_up_normal, -float_info.min, -0.0), (next_up_normal, +0.0, float_info.min), (next_down_normal, float_info.min, +0.0), (next_down_normal, -0.0, -float_info.min), ], ) def test_next_float_normal(func, val, expected): assert func(value=val, width=64, allow_subnormal=False) == expected ================================================ FILE: hypothesis-python/tests/cover/test_targeting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from operator import itemgetter import pytest from hypothesis import example, given, strategies as st, target from hypothesis.control import current_build_context from hypothesis.errors import InvalidArgument @example(0.0, "this covers the branch where context.data is None") @given( observation=st.integers() | st.floats(allow_nan=False, allow_infinity=False), label=st.text(), ) def test_allowed_inputs_to_target(observation, label): target(observation, label=label) @given( observation=st.integers(min_value=1) | st.floats(min_value=1, allow_nan=False, allow_infinity=False), label=st.sampled_from(["a", "few", "labels"]), ) def test_allowed_inputs_to_target_fewer_labels(observation, label): target(observation, label=label) @given(st.floats(min_value=1, max_value=10)) def test_target_without_label(observation): target(observation) @given( st.lists( st.tuples(st.floats(allow_nan=False, allow_infinity=False), st.text()), min_size=1, unique_by=itemgetter(1), ) ) def test_multiple_target_calls(args): for observation, label in args: target(observation, label=label) @given( st.lists(st.floats(allow_nan=False, allow_infinity=False), min_size=11, max_size=20) ) def test_respects_max_pool_size(observations): """Using many examples of several labels like this stresses the pool-size logic and internal assertions in TargetSelector. """ for i, obs in enumerate(observations): target(obs, label=str(i)) def everything_except(type_): # Note: we would usually stick to faster traditional or parametrized # tests to check that invalid inputs are rejected, but for `target()` # we need to use `@given` (to validate arguments instead of context) # so we might as well apply this neat recipe. return ( st.from_type(type) .flatmap(st.from_type) .filter(lambda x: not isinstance(x, type_)) ) @example(float("nan"), "") @example(float("inf"), "") @example(float("-inf"), "") @example("1", "Non-float observations are invalid") @example(0.0, ["a list of strings is not a valid label"]) @given(observation=everything_except((float, int)), label=everything_except(str)) def test_disallowed_inputs_to_target(observation, label): with pytest.raises(InvalidArgument): target(observation, label=label) def test_cannot_target_outside_test(): with pytest.raises(InvalidArgument): target(1.0, label="example label") @given(st.none()) def test_cannot_target_same_label_twice(_): if current_build_context().data.provider.avoid_realization: pytest.skip("target() is a noop to avoid realizing arguments") target(0.0, label="label") with pytest.raises(InvalidArgument): target(1.0, label="label") @given(st.none()) def test_cannot_target_default_label_twice(_): target(0.0) with pytest.raises(InvalidArgument): target(1.0) ================================================ FILE: hypothesis-python/tests/cover/test_testdecorators.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import functools import threading from collections import namedtuple import pytest from hypothesis import ( HealthCheck, Verbosity, assume, given, note, reporting, settings, strategies as st, ) from hypothesis.errors import Unsatisfiable from hypothesis.internal.conjecture.engine import INVALID_THRESHOLD_BASE from hypothesis.strategies import ( binary, booleans, builds, data, floats, frozensets, integers, just, lists, one_of, sampled_from, sets, text, ) from tests.common.utils import ( Why, assert_falsifying_output, capture_out, fails, fails_with, no_shrink, skipif_emscripten, xfail_on_crosshair, ) from tests.conjecture.common import buffer_size_limit @given(integers(), integers()) def test_int_addition_is_commutative(x, y): assert x + y == y + x @fails @given(text(min_size=1), text(min_size=1)) def test_str_addition_is_commutative(x, y): assert x + y == y + x @fails @given(binary(min_size=1), binary(min_size=1)) def test_bytes_addition_is_commutative(x, y): # We enforce min_size=1 here to avoid a rare flakiness, where the # test passes if x and/or y are b"" for every generated example. # (the internal implementation makes this less rare for bytes) assert x + y == y + x @given(integers(), integers(), integers()) def test_int_addition_is_associative(x, y, z): assert x + (y + z) == (x + y) + z @fails @given(floats(), floats(), floats()) @settings(max_examples=2000) def test_float_addition_is_associative(x, y, z): assert x + (y + z) == (x + y) + z @given(lists(integers())) def test_reversing_preserves_integer_addition(xs): assert sum(xs) == sum(reversed(xs)) def test_still_minimizes_on_non_assertion_failures(): @settings(max_examples=50) @given(integers()) def is_not_too_large(x): if x >= 10: raise ValueError(f"No, {x} is just too large. Sorry") with pytest.raises(ValueError, match=" 10 "): is_not_too_large() @given(integers()) def test_integer_division_shrinks_positive_integers(n): assume(n > 0) assert n / 2 < n class TestCases: @given(integers()) def test_abs_non_negative(self, x): assert abs(x) >= 0 assert isinstance(self, TestCases) @given(x=integers()) def test_abs_non_negative_varargs(self, x, *args): assert abs(x) >= 0 assert isinstance(self, TestCases) @given(x=integers()) def test_abs_non_negative_varargs_kwargs(self, *args, **kw): assert abs(kw["x"]) >= 0 assert isinstance(self, TestCases) @given(x=integers()) def test_abs_non_negative_varargs_kwargs_only(*args, **kw): assert abs(kw["x"]) >= 0 assert isinstance(args[0], TestCases) @fails @given(integers()) def test_int_is_always_negative(self, x): assert x < 0 @fails @given(floats(), floats()) def test_float_addition_cancels(self, x, y): assert x + (y - x) == y @fails @given(x=integers(min_value=0, max_value=3), name=text()) def test_can_be_given_keyword_args(x, name): assume(x > 0) assert len(name) < x @fails @given(one_of(floats(), booleans()), one_of(floats(), booleans())) def test_one_of_produces_different_values(x, y): assert type(x) == type(y) @given(just(42)) def test_is_the_answer(x): assert x == 42 @given(integers(1, 10)) def test_integers_are_in_range(x): assert 1 <= x <= 10 @given(integers(min_value=100)) def test_integers_from_are_from(x): assert x >= 100 def test_does_not_catch_interrupt_during_falsify(): called = False @given(integers()) def flaky_base_exception(x): nonlocal called if not called: called = True raise KeyboardInterrupt with pytest.raises(KeyboardInterrupt): flaky_base_exception() @given(lists(integers(), unique=True), integers()) def test_removing_an_element_from_a_unique_list(xs, y): assume(len(set(xs)) == len(xs)) try: xs.remove(y) except ValueError: pass assert y not in xs @fails @given(lists(integers(), min_size=2), data()) def test_removing_an_element_from_a_non_unique_list(xs, data): y = data.draw(sampled_from(xs)) xs.remove(y) assert y not in xs @given(sets(sampled_from(list(range(10))))) def test_can_test_sets_sampled_from(xs): assert all(isinstance(x, int) for x in xs) assert all(0 <= x < 10 for x in xs) mix = one_of(sampled_from([1, 2, 3]), text()) @fails @given(mix, mix) def test_can_mix_sampling_with_generating(x, y): assert type(x) == type(y) @fails @given(frozensets(integers())) def test_can_find_large_sum_frozenset(xs): assert sum(xs) < 100 def test_prints_on_failure_by_default(): @given(integers(), integers()) @settings(max_examples=1000) def test_ints_are_sorted(balthazar, evans): assume(evans >= 0) assert balthazar <= evans assert_falsifying_output(test_ints_are_sorted, balthazar=1, evans=0) def test_does_not_print_on_success(): @settings(verbosity=Verbosity.normal) @given(integers()) def test_is_an_int(x): return with capture_out() as out: test_is_an_int() out = out.getvalue() lines = [l.strip() for l in out.split("\n")] assert all(not l for l in lines), lines @given(sampled_from([1])) def test_can_sample_from_single_element(x): assert x == 1 @fails @given(lists(integers())) def test_list_is_sorted(xs): assert sorted(xs) == xs @fails @given(floats(1.0, 2.0)) def test_is_an_endpoint(x): assert x in {1.0, 2.0} def test_breaks_bounds(): @fails @given(x=integers()) @settings(derandomize=True, max_examples=10_000) def test_is_bounded(t, x): assert x < t for t in [1, 10, 100, 1000]: test_is_bounded(t) @given(x=booleans()) def test_can_test_kwargs_only_methods(**kwargs): assert isinstance(kwargs["x"], bool) @fails_with(UnicodeEncodeError) @given(text()) @settings(max_examples=100) def test_is_ascii(x): x.encode("ascii") @fails @given(text()) def test_is_not_ascii(x): try: x.encode("ascii") raise AssertionError except UnicodeEncodeError: pass @fails @given(text(min_size=2)) @settings(max_examples=100, derandomize=True) def test_can_find_string_with_duplicates(s): assert len(set(s)) == len(s) @fails @given(text(min_size=1)) @settings(derandomize=True) def test_has_ascii(x): if not x: return ascii_characters = ( "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \t\n" ) assert any(c in ascii_characters for c in x) @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_can_derandomize(): values = [] @fails @given(integers()) @settings(derandomize=True, database=None) def test_blah(x): values.append(x) assert x > 0 test_blah() assert values v1 = values values = [] test_blah() assert v1 == values def test_can_run_without_database(): @given(integers()) @settings(database=None) def test_blah(x): raise AssertionError with pytest.raises(AssertionError): test_blah() @skipif_emscripten def test_can_run_with_database_in_thread(): results = [] @given(integers()) def test_blah(x): raise ValueError def run_test(): try: test_blah() except ValueError: results.append("success") # Run once in the main thread and once in another thread. Execution is # strictly serial, so no need for locking. run_test() assert results == ["success"] thread = threading.Thread(target=run_test) thread.start() thread.join() assert results == ["success", "success"] @given(integers()) def test_can_call_an_argument_f(f): # See issue https://github.com/HypothesisWorks/hypothesis-python/issues/38 # for details pass Litter = namedtuple("Litter", ("kitten1", "kitten2")) @given(builds(Litter, integers(), integers())) def test_named_tuples_are_of_right_type(litter): assert isinstance(litter, Litter) @fails_with(AttributeError) @given(integers().map(lambda x: x.nope)) @settings(suppress_health_check=list(HealthCheck)) def test_fails_in_reify(x): pass @given(text("a")) def test_a_text(x): assert set(x).issubset(set("a")) @given(text("")) def test_empty_text(x): assert not x @given(text("abcdefg")) def test_mixed_text(x): assert set(x).issubset(set("abcdefg")) @xfail_on_crosshair(Why.other, strict=False) # runs ~five failing examples def test_when_set_to_no_simplifies_runs_failing_example_twice(): failing = [] @given(integers()) @settings( phases=no_shrink, max_examples=100, verbosity=Verbosity.normal, report_multiple_bugs=False, ) def foo(x): if x > 11: note("Lo") failing.append(x) raise AssertionError with pytest.raises(AssertionError) as err: foo() assert len(failing) == 2 assert len(set(failing)) == 1 assert "Falsifying example" in "\n".join(err.value.__notes__) assert "Lo" in err.value.__notes__ @given(integers().filter(lambda x: x % 4 == 0)) def test_filtered_values_satisfy_condition(i): assert i % 4 == 0 def nameless_const(x): def f(u, v): return u return functools.partial(f, x) @given(sets(booleans()).map(nameless_const(2))) def test_can_map_nameless(x): assert x == 2 @given(integers(0, 10).flatmap(nameless_const(just(3)))) def test_can_flatmap_nameless(x): assert x == 3 def test_can_be_used_with_none_module(): def test_is_cool(i): pass test_is_cool.__module__ = None test_is_cool = given(integers())(test_is_cool) test_is_cool() def test_does_not_print_notes_if_all_succeed(): @given(integers()) @settings(verbosity=Verbosity.normal) def test(i): note("Hi there") with capture_out() as out, reporting.with_reporter(reporting.default): test() assert not out.getvalue() def test_prints_notes_once_on_failure(): @given(lists(integers())) @settings(database=None, verbosity=Verbosity.normal) def test(xs): note("Hi there") if sum(xs) <= 100: raise ValueError with pytest.raises(ValueError) as err: test() assert err.value.__notes__.count("Hi there") == 1 @given(lists(integers(), max_size=0)) def test_empty_lists(xs): assert xs == [] @xfail_on_crosshair(Why.other, strict=False) def test_given_usable_inline_on_lambdas(): xs = [] given(booleans())(lambda x: xs.append(x))() assert len(xs) == 2 assert set(xs) == {False, True} def test_notes_high_filter_rates_in_unsatisfiable_error(): @given(st.integers()) @settings(suppress_health_check=[HealthCheck.filter_too_much]) def f(v): assume(False) with pytest.raises( Unsatisfiable, match=( rf"Unable to satisfy assumptions of f\. {INVALID_THRESHOLD_BASE+1} of " rf"{INVALID_THRESHOLD_BASE+1} examples failed a \.filter\(\) or assume\(\)" ), ): f() # crosshair generates one valid input before verifying the test function, # so the Unsatisfiable check never occurs. # (not strict due to slowness causing crosshair to bail out on the first input, # maybe?) @xfail_on_crosshair(Why.other, strict=False) def test_notes_high_overrun_rates_in_unsatisfiable_error(): @given(st.binary(min_size=100)) @settings( suppress_health_check=[ HealthCheck.data_too_large, HealthCheck.too_slow, HealthCheck.large_base_example, ] ) def f(v): pass match = ( rf"{INVALID_THRESHOLD_BASE+1} of {INVALID_THRESHOLD_BASE+1} examples were too large to" rf" finish generating; try reducing the typical size of your inputs\?" ) with ( pytest.raises(Unsatisfiable, match=match), buffer_size_limit(10), ): f() ================================================ FILE: hypothesis-python/tests/cover/test_threading.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from threading import Barrier, Thread import pytest from hypothesis import given, settings, strategies as st from hypothesis.utils.threading import ThreadLocal from tests.common.utils import skipif_emscripten def test_threadlocal_setattr_and_getattr(): threadlocal = ThreadLocal(a=lambda: 1, b=lambda: 2) assert threadlocal.a == 1 assert threadlocal.b == 2 # check that we didn't add attributes to the ThreadLocal instance itself # instead of its threading.local() variable assert set(threadlocal.__dict__) == { "_ThreadLocal__initialized", "_ThreadLocal__kwargs", "_ThreadLocal__threadlocal", } threadlocal.a = 3 assert threadlocal.a == 3 assert threadlocal.b == 2 assert set(threadlocal.__dict__) == { "_ThreadLocal__initialized", "_ThreadLocal__kwargs", "_ThreadLocal__threadlocal", } def test_nonexistent_getattr_raises(): threadlocal = ThreadLocal(a=lambda: 1) with pytest.raises(AttributeError): threadlocal.c def test_nonexistent_setattr_raises(): threadlocal = ThreadLocal(a=lambda: 1) with pytest.raises(AttributeError): threadlocal.c = 2 def test_raises_if_not_passed_callable(): with pytest.raises(TypeError): ThreadLocal(a=1) @skipif_emscripten @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="crosshair is not thread-safe", ) def test_run_given_concurrently(): # this is just a basic covering test. The more complicated and complete threading # tests are in nocover/test_threading.py. n_threads = 2 barrier = Barrier(n_threads) @given(st.integers()) def test(n): barrier.wait() threads = [Thread(target=test) for _ in range(n_threads)] for thread in threads: thread.start() for thread in threads: thread.join(timeout=10) # rely on our pytest-run-parallel job to test this, since this only happens because # pytest instantiates a new class instance for each parametrization. class TestNoDifferingExecutorsHealthCheck: @given(st.integers()) @pytest.mark.parametrize("x", range(2)) def test_a(self, x, i): pass ================================================ FILE: hypothesis-python/tests/cover/test_traceback_elision.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import traceback import pytest from hypothesis import Verbosity, given, settings, strategies as st @pytest.mark.parametrize("verbosity", [Verbosity.normal, Verbosity.debug]) @pytest.mark.parametrize("env_value", ["", "1"]) def test_tracebacks_omit_hypothesis_internals(monkeypatch, env_value, verbosity): monkeypatch.setenv("HYPOTHESIS_NO_TRACEBACK_TRIM", env_value) @settings(verbosity=verbosity) @given(st.just(False)) def simplest_failure(x): raise ValueError try: simplest_failure() except ValueError as e: tb = traceback.extract_tb(e.__traceback__) # Unless in debug mode, Hypothesis adds 1 frame - the least possible! # (4 frames: this one, simplest_failure, internal frame, assert False) if verbosity < Verbosity.debug and not env_value: assert len(tb) == 4 else: assert len(tb) >= 5 ================================================ FILE: hypothesis-python/tests/cover/test_type_lookup.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import abc import enum import typing from collections.abc import Callable, Sequence from inspect import Parameter as P, Signature from typing import Generic, List as _List, TypeVar, Union import pytest from hypothesis import given, infer, settings, strategies as st from hypothesis.errors import InvalidArgument, ResolutionFailed from hypothesis.internal.compat import get_type_hints from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal import types from hypothesis.strategies._internal.lazy import LazyStrategy from hypothesis.strategies._internal.types import _global_type_lookup from hypothesis.strategies._internal.utils import _all_strategies from tests.common.debug import ( assert_all_examples, assert_simple_property, check_can_generate_examples, find_any, ) from tests.common.utils import ( Why, fails_with, skipif_threading, temp_registered, xfail_on_crosshair, ) # we'll continue testing the typing variants until their removal from the stdlib # ruff: noqa: UP006, UP035, UP007 types_with_core_strat = { type_ for type_, strat in _global_type_lookup.items() if isinstance(strat, LazyStrategy) and strat.function in vars(st).values() } def test_generic_sequence_of_integers_may_be_lists_or_bytes(): strat = st.from_type(Sequence[int]) find_any(strat, lambda x: isinstance(x, bytes)) find_any(strat, lambda x: isinstance(x, list)) @pytest.mark.parametrize("typ", sorted(types_with_core_strat, key=str)) def test_resolve_core_strategies(typ): @given(st.from_type(typ)) def inner(ex): assert isinstance(ex, typ) inner() def test_lookup_knows_about_all_core_strategies(): # Build a set of all types output by core strategies blocklist = { "builds", "data", "deferred", "from_regex", "from_type", "ip_addresses", "iterables", "just", "nothing", "one_of", "permutations", "random_module", "randoms", "recursive", "runner", "sampled_from", "shared", "timezone_keys", "timezones", } assert set(_all_strategies).issuperset(blocklist), blocklist.difference( _all_strategies ) found = set() for thing in ( getattr(st, name) for name in sorted(_all_strategies) if name in dir(st) and name not in blocklist ): for n in range(3): try: ex = find_any(thing(*([st.nothing()] * n))) found.add(type(ex)) break except Exception: continue cannot_lookup = found - set(types._global_type_lookup) assert not cannot_lookup def test_lookup_keys_are_types(): with pytest.raises(InvalidArgument): st.register_type_strategy("int", st.integers()) assert "int" not in types._global_type_lookup @pytest.mark.parametrize( "typ, not_a_strategy", [ (int, 42), # Values must be strategies # Can't register NotImplemented directly, even though strategy functions # can return it. (int, NotImplemented), ], ) def test_lookup_values_are_strategies(typ, not_a_strategy): with pytest.raises(InvalidArgument): st.register_type_strategy(typ, not_a_strategy) assert not_a_strategy not in types._global_type_lookup.values() @pytest.mark.parametrize("typ", sorted(types_with_core_strat, key=str)) def test_lookup_overrides_defaults(typ): sentinel = object() with temp_registered(typ, st.just(sentinel)): assert_simple_property(st.from_type(typ), lambda v: v is sentinel) assert_simple_property(st.from_type(typ), lambda v: v is not sentinel) class ParentUnknownType: pass class UnknownType(ParentUnknownType): def __init__(self, arg): pass def test_custom_type_resolution_fails_without_registering(): fails = st.from_type(UnknownType) with pytest.raises(ResolutionFailed): check_can_generate_examples(fails) def test_custom_type_resolution(): sentinel = object() with temp_registered(UnknownType, st.just(sentinel)): assert_simple_property(st.from_type(UnknownType), lambda v: v is sentinel) # Also covered by registration of child class assert_simple_property(st.from_type(ParentUnknownType), lambda v: v is sentinel) def test_custom_type_resolution_with_function(): sentinel = object() with temp_registered(UnknownType, lambda _: st.just(sentinel)): assert_simple_property(st.from_type(UnknownType), lambda v: v is sentinel) assert_simple_property(st.from_type(ParentUnknownType), lambda v: v is sentinel) def test_custom_type_resolution_with_function_non_strategy(): with temp_registered(UnknownType, lambda _: None): with pytest.raises(ResolutionFailed): check_can_generate_examples(st.from_type(UnknownType)) with pytest.raises(ResolutionFailed): check_can_generate_examples(st.from_type(ParentUnknownType)) @pytest.mark.parametrize("strategy_returned", [True, False]) def test_conditional_type_resolution_with_function(strategy_returned): sentinel = object() def resolve_strategy(thing): assert thing == UnknownType if strategy_returned: return st.just(sentinel) return NotImplemented with temp_registered(UnknownType, resolve_strategy): if strategy_returned: assert_simple_property(st.from_type(UnknownType), lambda v: v is sentinel) else: with pytest.raises(ResolutionFailed): check_can_generate_examples(st.from_type(UnknownType)) def test_errors_if_generic_resolves_empty(): with temp_registered(UnknownType, lambda _: st.nothing()): fails_1 = st.from_type(UnknownType) with pytest.raises(ResolutionFailed): check_can_generate_examples(fails_1) fails_2 = st.from_type(ParentUnknownType) with pytest.raises(ResolutionFailed): check_can_generate_examples(fails_2) @skipif_threading def test_cannot_register_empty(): # Cannot register and did not register with pytest.raises(InvalidArgument): st.register_type_strategy(UnknownType, st.nothing()) fails = st.from_type(UnknownType) with pytest.raises(ResolutionFailed): check_can_generate_examples(fails) assert UnknownType not in types._global_type_lookup def test_pulic_interface_works(): check_can_generate_examples(st.from_type(int)) fails = st.from_type("not a type or annotated function") with pytest.raises(InvalidArgument): check_can_generate_examples(fails) @pytest.mark.parametrize("infer_token", [infer, ...]) def test_given_can_infer_from_manual_annotations(infer_token): # Editing annotations before decorating is hilariously awkward, but works! def inner(a): assert isinstance(a, int) inner.__annotations__ = {"a": int} given(a=infer_token)(inner)() class EmptyEnum(enum.Enum): pass @fails_with(InvalidArgument) @given(st.from_type(EmptyEnum)) def test_error_if_enum_is_empty(x): pass class BrokenClass: __init__ = "Hello!" def test_uninspectable_builds(): with pytest.raises(TypeError, match="object is not callable"): check_can_generate_examples(st.builds(BrokenClass)) def test_uninspectable_from_type(): with pytest.raises(TypeError, match="object is not callable"): check_can_generate_examples(st.from_type(BrokenClass)) def _check_instances(t): # See https://github.com/samuelcolvin/pydantic/discussions/2508 return ( t.__module__ != "typing" and t.__name__ != "ByteString" and not t.__module__.startswith("pydantic") and t.__module__ != "typing_extensions" ) def maybe_mark(x): if x.__name__ in "Match Decimal IPv4Address": marks = xfail_on_crosshair(Why.other, as_marks=True, strict=False) return pytest.param(x, marks=marks) return x @pytest.mark.parametrize( "typ", sorted( (maybe_mark(x) for x in _global_type_lookup if _check_instances(x)), key=str, ), ) @given(data=st.data()) def test_can_generate_from_all_registered_types(data, typ): value = data.draw(st.from_type(typ), label="value") assert isinstance(value, typ) T = TypeVar("T") class MyGeneric(Generic[T]): def __init__(self, arg: T) -> None: self.arg = arg class Lines(Sequence[str]): """Represent a sequence of text lines. It turns out that resolving a class which inherits from a parametrised generic type is... tricky. See https://github.com/HypothesisWorks/hypothesis/issues/2951 """ class SpecificDict(dict[int, int]): pass def using_generic(instance: MyGeneric[T]) -> T: return instance.arg def using_concrete_generic(instance: MyGeneric[int]) -> int: return instance.arg def test_generic_origin_empty(): with pytest.raises(ResolutionFailed): check_can_generate_examples(st.builds(using_generic)) def test_issue_2951_regression(): lines_strat = st.builds(Lines, lines=st.lists(st.text())) prev_seq_int_repr = repr(st.from_type(Sequence[int])) with temp_registered(Lines, lines_strat): assert st.from_type(Lines) == lines_strat # Now let's test that the strategy for ``Sequence[int]`` did not # change just because we registered a strategy for ``Lines``: assert repr(st.from_type(Sequence[int])) == prev_seq_int_repr def test_issue_2951_regression_two_params(): map_strat = st.builds(SpecificDict, st.dictionaries(st.integers(), st.integers())) expected = repr(st.from_type(dict[int, int])) with temp_registered(SpecificDict, map_strat): assert st.from_type(SpecificDict) == map_strat assert expected == repr(st.from_type(dict[int, int])) @pytest.mark.parametrize( "generic", ( Union[str, int], str | int, Sequence[Sequence[int]], MyGeneric[str], Callable[..., str], Callable[[int], str], ), ids=repr, ) @pytest.mark.parametrize("strategy", [st.none(), lambda _: st.none()]) def test_generic_origin_with_type_args(generic, strategy): with pytest.raises(InvalidArgument): st.register_type_strategy(generic, strategy) assert generic not in types._global_type_lookup @pytest.mark.parametrize( "generic", ( Callable, list, Sequence, # you can register types with all generic parameters _List[T], getattr(typing, "Sequence", None)[T], # pyupgrade workaround list[T], Sequence[T], # User-defined generics should also work MyGeneric, MyGeneric[T], ), ) def test_generic_origin_without_type_args(generic): with temp_registered(generic, st.just("example")): pass @pytest.mark.parametrize( "strat, type_", [ (st.from_type, MyGeneric[T]), (st.from_type, MyGeneric[int]), (st.from_type, MyGeneric), (st.builds, using_generic), (st.builds, using_concrete_generic), ], ids=get_pretty_function_description, ) def test_generic_origin_from_type(strat, type_): with temp_registered(MyGeneric, st.builds(MyGeneric)): check_can_generate_examples(strat(type_)) def test_generic_origin_concrete_builds(): with temp_registered(MyGeneric, st.builds(MyGeneric, st.integers())): assert_all_examples( st.builds(using_generic), lambda example: isinstance(example, int) ) class AbstractFoo(abc.ABC): def __init__(self, x): # noqa: B027 pass @abc.abstractmethod def qux(self): pass class ConcreteFoo1(AbstractFoo): # Can't resolve this one due to unannotated `x` param def qux(self): pass class ConcreteFoo2(AbstractFoo): def __init__(self, x: int): pass def qux(self): pass @given(st.from_type(AbstractFoo)) def test_gen_abstract(foo): # This requires that we correctly checked which of the subclasses # could be resolved, rather than unconditionally using all of them. assert isinstance(foo, ConcreteFoo2) class AbstractBar(abc.ABC): def __init__(self, x): # noqa: B027 pass @abc.abstractmethod def qux(self): pass class ConcreteBar(AbstractBar): def qux(self): pass def test_abstract_resolver_fallback(): # We create our distinct strategies for abstract and concrete types gen_abstractbar = st.from_type(AbstractBar) gen_concretebar = st.builds(ConcreteBar, x=st.none()) assert gen_abstractbar != gen_concretebar # And trying to generate an instance of the abstract type fails, # UNLESS the concrete type is currently resolvable with pytest.raises(ResolutionFailed): check_can_generate_examples(gen_abstractbar) with temp_registered(ConcreteBar, gen_concretebar): # which in turn means we resolve to the concrete subtype. assert_simple_property( gen_abstractbar, lambda gen: isinstance(gen, ConcreteBar) ) with pytest.raises(ResolutionFailed): check_can_generate_examples(gen_abstractbar) def _one_arg(x: int): assert isinstance(x, int) def _multi_arg(x: int, y: str): assert isinstance(x, int) assert isinstance(y, str) def _kwd_only(*, y: str): assert isinstance(y, str) def _pos_and_kwd_only(x: int, *, y: str): assert isinstance(x, int) assert isinstance(y, str) @pytest.mark.parametrize("func", [_one_arg, _multi_arg, _kwd_only, _pos_and_kwd_only]) def test_infer_all(func): # tests @given(...) against various signatures settings(max_examples=1)(given(...))(func)() def test_does_not_add_param_empty_to_type_hints(): def f(x): pass f.__signature__ = Signature([P("y", P.KEYWORD_ONLY)], return_annotation=None) assert get_type_hints(f) == {} ================================================ FILE: hypothesis-python/tests/cover/test_type_lookup_forward_ref.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ We need these test to make sure ``TypeVar('X', bound='MyType')`` works correctly. There was a problem previously that ``bound='MyType'`` was resolved as ``ForwardRef('MyType')`` which is not a real type. And ``hypothesis`` was not able to generate any meaningful values out of it. Right here we test different possible outcomes for different Python versions: - Regular case, when ``'MyType'`` can be imported - Alias case, when we use type aliases for ``'MyType'`` - ``if TYPE_CHECKING:`` case, when ``'MyType'`` only exists during type checking and is not importable at all - Dot access case, like ``'module.MyType'`` - Missing case, when there's no ``'MyType'`` at all """ from typing import TYPE_CHECKING, ForwardRef, TypeVar import pytest from hypothesis import given, strategies as st from hypothesis.errors import ResolutionFailed from tests.common import utils from tests.common.debug import check_can_generate_examples if TYPE_CHECKING: from tests.common.utils import ExcInfo # we just need any type # Correct: _Correct = TypeVar("_Correct", bound="CustomType") def correct_fun(thing: _Correct) -> int: return thing.arg class CustomType: def __init__(self, arg: int) -> None: self.arg = arg @given(st.builds(correct_fun)) def test_bound_correct_forward_ref(built): """Correct resolution of existing type codebase.""" assert isinstance(built, int) # Aliases: _Alias = TypeVar("_Alias ", bound="OurAlias") def alias_fun(thing: _Alias) -> int: return thing.arg OurAlias = CustomType @given(st.builds(alias_fun)) def test_bound_alias_forward_ref(built): """Correct resolution of type aliases.""" assert isinstance(built, int) # Dot access: _CorrectDotAccess = TypeVar("_CorrectDotAccess", bound="utils.ExcInfo") _WrongDotAccess = TypeVar("_WrongDotAccess", bound="wrong.ExcInfo") # noqa _MissingDotAccess = TypeVar("_MissingDotAccess", bound="utils.MissingType") def correct_dot_access_fun(thing: _CorrectDotAccess) -> int: return 1 def wrong_dot_access_fun(thing: _WrongDotAccess) -> int: return 1 def missing_dot_access_fun(thing: _MissingDotAccess) -> int: return 1 @given(st.builds(correct_dot_access_fun)) def test_bound_correct_dot_access_forward_ref(built): """Correct resolution of dot access types.""" assert isinstance(built, int) @pytest.mark.parametrize("function", [wrong_dot_access_fun, missing_dot_access_fun]) def test_bound_missing_dot_access_forward_ref(function): """Resolution of missing type in dot access.""" with pytest.raises(ResolutionFailed): check_can_generate_examples(st.builds(function)) # Missing: _Missing = TypeVar("_Missing", bound="MissingType") # noqa def missing_fun(thing: _Missing) -> int: return 1 def test_bound_missing_forward_ref(): """We should raise proper errors on missing types.""" with pytest.raises(ResolutionFailed): check_can_generate_examples(st.builds(missing_fun)) # Type checking only: _TypeChecking = TypeVar("_TypeChecking", bound="ExcInfo") def typechecking_only_fun(thing: _TypeChecking) -> int: return 1 def test_bound_type_cheking_only_forward_ref(): """We should fallback to registering explicit ``ForwardRef`` when we have to.""" with utils.temp_registered(ForwardRef("ExcInfo"), st.just(1)): check_can_generate_examples(st.builds(typechecking_only_fun)) def test_bound_type_checking_only_forward_ref_wrong_type(): """We should check ``ForwardRef`` parameter name correctly.""" with ( utils.temp_registered(ForwardRef("WrongType"), st.just(1)), pytest.raises(ResolutionFailed), ): check_can_generate_examples(st.builds(typechecking_only_fun)) ================================================ FILE: hypothesis-python/tests/cover/test_typealias_py312.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Callable from typing import get_args, get_origin import pytest from hypothesis import strategies as st from hypothesis.errors import HypothesisException from hypothesis.strategies._internal.types import evaluate_type_alias_type from tests.common.debug import assert_simple_property, find_any from tests.common.utils import temp_registered def test_resolves_simple_typealias(): type MyInt = int type AliasedInt = MyInt type MaybeInt = int | None assert_simple_property(st.from_type(MyInt), lambda x: isinstance(x, int)) assert_simple_property(st.from_type(AliasedInt), lambda x: isinstance(x, int)) assert_simple_property( st.from_type(MaybeInt), lambda x: isinstance(x, int) or x is None ) find_any(st.from_type(MaybeInt), lambda x: isinstance(x, int)) find_any(st.from_type(MaybeInt), lambda x: x is None) def test_resolves_nested(): type Point1 = int type Point2 = Point1 type Point3 = Point2 assert_simple_property(st.from_type(Point3), lambda x: isinstance(x, int)) def test_resolves_parametrized(): type MyList = list[int] assert_simple_property( st.from_type(MyList), lambda l: all(isinstance(x, int) for x in l) ) def test_mutually_recursive_fails(): # example from # https://docs.python.org/3/library/typing.html#typing.TypeAliasType.__value__ type A = B type B = A # I guess giving a nicer error here would be good, but detecting this in general # is...complicated. with pytest.raises(RecursionError): find_any(st.from_type(A)) def test_mutually_recursive_fails_parametrized(): # same with parametrized types type A[T] = B[T] type B[T] = A[T] with pytest.raises(RecursionError): find_any(st.from_type(A[int])) def test_can_register_typealias(): type A = int st.register_type_strategy(A, st.just("a")) assert_simple_property(st.from_type(A), lambda x: x == "a") def test_prefers_manually_registered_typealias(): # manually registering a `type A = ...` should override automatic detection type A = int assert_simple_property(st.from_type(A), lambda x: isinstance(x, int)) with temp_registered(A, st.booleans()): assert_simple_property(st.from_type(A), lambda x: isinstance(x, bool)) def test_resolves_parameterized_typealias(): type A[T] = list[T] assert_simple_property(st.from_type(A[int]), lambda x: isinstance(x, list)) find_any(st.from_type(A[int]), lambda x: len(x) > 0) assert_simple_property( st.from_type(A[int]), lambda x: all(isinstance(i, int) for i in x) ) def test_resolves_nested_parameterized_typealias(): type Inner[T] = list[T] type Outer[T] = Inner[T] assert_simple_property(st.from_type(Outer[str]), lambda x: isinstance(x, list)) assert_simple_property( st.from_type(Outer[str]), lambda x: all(isinstance(i, str) for i in x) ) def test_resolves_parameterized_typealias_with_literal_types(): # Type param used in non-first position with literal types type MyDict[T] = dict[str, T] assert_simple_property(st.from_type(MyDict[int]), lambda x: isinstance(x, dict)) assert_simple_property( st.from_type(MyDict[int]), lambda x: all(isinstance(k, str) and isinstance(v, int) for k, v in x.items()), ) def test_can_register_parameterized_typealias_with_unused_params(): # Users can explicitly register strategies for such types using a resolver function type MyList[T1, T2] = list[T1] # Register a function that resolves the type alias def resolve_mylist(thing): if get_origin(thing) is MyList: args = get_args(thing) # Use the first type argument, ignore the second return st.lists(st.from_type(args[0])) return NotImplemented st.register_type_strategy(MyList, resolve_mylist) assert_simple_property( st.from_type(MyList[int, float]), lambda x: isinstance(x, list) ) assert_simple_property( st.from_type(MyList[int, float]), lambda x: all(isinstance(i, int) for i in x) ) def test_typealias_evaluation(): type A[T1, T2] = list[T1] assert evaluate_type_alias_type(A[int, float]) == list[int] type A[T1, T2] = list[T2] assert evaluate_type_alias_type(A[float, int]) == list[int] type A[K, V] = dict[V, K] assert evaluate_type_alias_type(A[str, int]) == dict[int, str] type A[T] = list[list[T]] assert evaluate_type_alias_type(A[int]) == list[list[int]] type Inner[T] = list[T] type Outer[T] = Inner[T] assert evaluate_type_alias_type(Outer[int]) == list[int] type Bare[T] = list assert evaluate_type_alias_type(Bare[int]) == list type A[T1, T2] = list[T1] assert evaluate_type_alias_type(A[int]) == list[int] # tries to reference free variable type A[T1, T2] = list[T2] with pytest.raises(ValueError): evaluate_type_alias_type(A[int]) # (currently) unsupported type forms type A[*Ts] = tuple[*Ts] with pytest.raises(HypothesisException, match="Hypothesis does not yet support"): assert evaluate_type_alias_type(A[int, str, float]) == tuple[int, str, float] type A[**P] = Callable[P, int] with pytest.raises(HypothesisException, match="Hypothesis does not yet support"): assert evaluate_type_alias_type(A[[str, float]]) == Callable[[str, float], int] ================================================ FILE: hypothesis-python/tests/cover/test_unicode_identifiers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st from hypothesis.internal.reflection import get_pretty_function_description, proxies def test_can_copy_signature_of_unicode_args(): def foo(μ): return μ @proxies(foo) def bar(μ): return foo(μ) assert bar(1) == 1 def test_can_copy_signature_of_unicode_name(): def ā(): return 1 @proxies(ā) def bar(): return 2 assert bar() == 2 is_approx_π = lambda x: x == 3.1415 def test_can_handle_unicode_identifier_in_same_line_as_lambda_def(): assert get_pretty_function_description(is_approx_π) == "lambda x: x == 3.1415" def test_regression_issue_1700(): π = 3.1415 @given(st.floats(min_value=-π, max_value=π).filter(lambda x: abs(x) > 1e-5)) def test_nonzero(x): assert x != 0 test_nonzero() ================================================ FILE: hypothesis-python/tests/cover/test_unittest.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import io import sys import unittest import pytest from hypothesis import given, strategies as st from hypothesis.errors import FailedHealthCheck, HypothesisWarning from tests.common.utils import fails_with, skipif_emscripten class Thing_with_a_subThing(unittest.TestCase): """Example test case using subTest for the actual test below.""" @given(st.tuples(st.booleans(), st.booleans())) def thing(self, lst): for i, b in enumerate(lst): with pytest.warns(HypothesisWarning), self.subTest((i, b)): self.assertTrue(b) def test_subTest(): suite = unittest.TestSuite() suite.addTest(Thing_with_a_subThing("thing")) stream = io.StringIO() out = unittest.TextTestRunner(stream=stream).run(suite) assert len(out.failures) <= out.testsRun, out class test_given_on_setUp_fails_health_check(unittest.TestCase): @fails_with(FailedHealthCheck) @given(st.integers()) def setUp(self, i): pass def test(self): """Provide something to set up for, so the setUp method is called.""" SUBTEST_SUITE = """ import unittest from hypothesis import given, settings, strategies as st class MyTest(unittest.TestCase): @given(s=st.text()) @settings(deadline=None) def test_subtest(self, s): with self.subTest(text=s): self.assertIsInstance(s, str) if __name__ == "__main__": unittest.main() """ @skipif_emscripten @pytest.mark.parametrize("err", [[], ["-Werror"]]) def test_subTest_no_self(testdir, err): # https://github.com/HypothesisWorks/hypothesis/issues/2462 # for some reason this issue happens only when running unittest from commandline fname = testdir.makepyfile(tests=SUBTEST_SUITE) result = testdir.run(sys.executable, *err, str(fname)) expected = pytest.ExitCode.TESTS_FAILED if err else pytest.ExitCode.OK assert result.ret == expected, result.stderr.str() ================================================ FILE: hypothesis-python/tests/cover/test_uuids.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import uuid import pytest from hypothesis import strategies as st from hypothesis.errors import InvalidArgument from tests.common.debug import assert_no_examples, check_can_generate_examples, find_any def test_no_nil_uuid_by_default(): assert_no_examples(st.uuids(), lambda x: x == uuid.UUID(int=0)) def test_can_generate_nil_uuid(): find_any(st.uuids(allow_nil=True), lambda x: x == uuid.UUID(int=0)) def test_can_only_allow_nil_uuid_with_none_version(): check_can_generate_examples(st.uuids(version=None, allow_nil=True)) with pytest.raises(InvalidArgument): check_can_generate_examples(st.uuids(version=4, allow_nil=True)) with pytest.raises(InvalidArgument): check_can_generate_examples(st.uuids(version=None, allow_nil="not a bool")) ================================================ FILE: hypothesis-python/tests/cover/test_validation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import functools import warnings import pytest from hypothesis import find, given, strategies as st from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.internal.validation import check_type from hypothesis.strategies import ( SearchStrategy as ActualSearchStrategy, binary, booleans, data, dictionaries, floats, frozensets, integers, lists, nothing, recursive, sets, text, ) from hypothesis.strategies._internal.strategies import check_strategy from tests.common.debug import check_can_generate_examples, find_any from tests.common.utils import fails_with def test_errors_when_given_varargs(): @given(integers()) def has_varargs(*args): pass with pytest.raises(InvalidArgument) as e: has_varargs() assert "varargs" in e.value.args[0] def test_varargs_without_positional_arguments_allowed(): @given(somearg=integers()) def has_varargs(somearg, *args): pass def test_errors_when_given_varargs_and_kwargs_with_positional_arguments(): @given(integers()) def has_varargs(*args, **kw): pass with pytest.raises(InvalidArgument) as e: has_varargs() assert "varargs" in e.value.args[0] def test_varargs_and_kwargs_without_positional_arguments_allowed(): @given(somearg=integers()) def has_varargs(*args, **kw): pass def test_bare_given_errors(): @given() def test(): pass with pytest.raises(InvalidArgument): test() def test_errors_on_unwanted_kwargs(): @given(hello=int, world=int) def greet(world): pass with pytest.raises(InvalidArgument): greet() def test_errors_on_too_many_positional_args(): @given(integers(), int, int) def foo(x, y): pass with pytest.raises(InvalidArgument): foo() def test_errors_on_any_varargs(): @given(integers()) def oops(*args): pass with pytest.raises(InvalidArgument): oops() def test_can_put_arguments_in_the_middle(): @given(y=integers()) def foo(x, y, z): pass foo(1, 2) def test_float_ranges(): with pytest.raises(InvalidArgument): check_can_generate_examples(floats(float("nan"), 0)) with pytest.raises(InvalidArgument): check_can_generate_examples(floats(1, -1)) def test_float_range_and_allow_nan_cannot_both_be_enabled(): with pytest.raises(InvalidArgument): check_can_generate_examples(floats(min_value=1, allow_nan=True)) with pytest.raises(InvalidArgument): check_can_generate_examples(floats(max_value=1, allow_nan=True)) def test_float_finite_range_and_allow_infinity_cannot_both_be_enabled(): with pytest.raises(InvalidArgument): check_can_generate_examples(floats(0, 1, allow_infinity=True)) def test_does_not_error_if_min_size_is_bigger_than_default_size(): find_any(lists(integers(), min_size=50)) find_any(sets(integers(), min_size=50)) find_any(frozensets(integers(), min_size=50)) find_any(lists(integers(), min_size=50, unique=True)) def test_list_unique_and_unique_by_cannot_both_be_enabled(): @given(lists(integers(), unique=True, unique_by=lambda x: x)) def boom(t): pass with pytest.raises(InvalidArgument) as e: boom() assert "unique " in e.value.args[0] assert "unique_by" in e.value.args[0] def test_min_before_max(): with pytest.raises(InvalidArgument): integers(min_value=1, max_value=0).validate() def test_filter_validates(): with pytest.raises(InvalidArgument): integers(min_value=1, max_value=0).filter(bool).validate() def test_recursion_validates_base_case(): with pytest.raises(InvalidArgument): recursive(integers(min_value=1, max_value=0), lists).validate() def test_recursion_validates_recursive_step(): with pytest.raises(InvalidArgument): recursive(integers(), lambda x: lists(x, min_size=3, max_size=1)).validate() @fails_with(InvalidArgument) @given(x=integers()) def test_stuff_keyword(x=1): # noqa: PT028 pass @fails_with(InvalidArgument) @given(integers()) def test_stuff_positional(x=1): # noqa: PT028 pass @fails_with(InvalidArgument) @given(integers(), integers()) def test_too_many_positional(x): pass def test_given_warns_on_use_of_non_strategies(): @given(bool) def test(x): pass with pytest.raises(InvalidArgument): test() def test_given_warns_when_mixing_positional_with_keyword(): @given(booleans(), y=booleans()) def test(x, y): pass with pytest.raises(InvalidArgument): test() def test_cannot_find_non_strategies(): with pytest.raises(InvalidArgument): find(bool, bool) @pytest.mark.parametrize( "strategy", [ functools.partial(lists, elements=integers()), functools.partial(dictionaries, keys=integers(), values=integers()), text, binary, ], ) @pytest.mark.parametrize("min_size,max_size", [(0, "10"), ("0", 10)]) def test_valid_sizes(strategy, min_size, max_size): @given(strategy(min_size=min_size, max_size=max_size)) def test(x): pass with pytest.raises(InvalidArgument): test() def test_check_type_with_tuple_of_length_two(): def type_checker(x): check_type((int, str), x, "x") type_checker(1) type_checker("1") with pytest.raises(InvalidArgument, match="Expected one of int, str but got "): type_checker(1.0) def test_validation_happens_on_draw(): @given(data()) def test(data): data.draw(integers().flatmap(lambda _: lists(nothing(), min_size=1))) with pytest.raises(InvalidArgument, match="has no values"): test() class SearchStrategy: """Not the SearchStrategy type you were looking for.""" def check_type_(*args): return check_type(*args) def test_check_type_suggests_check_strategy(): check_type_(SearchStrategy, SearchStrategy(), "this is OK") with pytest.raises(AssertionError, match="use check_strategy instead"): check_type_(ActualSearchStrategy, None, "SearchStrategy assertion") def check_strategy_(*args): return check_strategy(*args) def test_check_strategy_might_suggest_sampled_from(): with pytest.raises(InvalidArgument) as excinfo: check_strategy_("not a strategy") assert "sampled_from" not in str(excinfo.value) with pytest.raises(InvalidArgument, match="such as st\\.sampled_from"): check_strategy_([1, 2, 3]) with pytest.raises(InvalidArgument, match="such as st\\.sampled_from"): check_strategy_((1, 2, 3)) check_strategy_(integers(), "passes for our custom coverage check") @pytest.mark.parametrize("codec", ["ascii", "utf-8"]) def test_warn_on_strings_matching_common_codecs(codec): with pytest.warns( HypothesisWarning, match=f"it seems like you are trying to use the codec {codec!r}", ): @given(st.text(codec)) def f(s): pass f() # if we reorder, it doesn't warn anymore with warnings.catch_warnings(): warnings.simplefilter("error") @given(st.text(codec[1:] + codec[:1])) def f(s): pass f() ================================================ FILE: hypothesis-python/tests/cover/test_verbosity.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from contextlib import contextmanager from hypothesis import example, find, given from hypothesis._settings import Verbosity, settings from hypothesis.reporting import default as default_reporter, with_reporter from hypothesis.strategies import booleans, integers, lists from tests.common.debug import minimal from tests.common.utils import Why, capture_out, fails, xfail_on_crosshair @contextmanager def capture_verbosity(): with capture_out() as o, with_reporter(default_reporter): yield o def test_prints_intermediate_in_success(): with capture_verbosity() as o: @settings(verbosity=Verbosity.verbose) @given(booleans()) def test_works(x): pass test_works() assert "Trying example" in o.getvalue() def test_does_not_log_in_quiet_mode(): with capture_verbosity() as o: @fails @settings(verbosity=Verbosity.quiet, print_blob=False) @given(integers()) def test_foo(x): raise AssertionError test_foo() assert not o.getvalue() def test_includes_progress_in_verbose_mode(): with capture_verbosity() as o: minimal( lists(integers(), min_size=1), lambda x: sum(x) >= 100, settings(verbosity=Verbosity.verbose), ) out = o.getvalue() assert out assert "Trying example: " in out @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_prints_initial_attempts_on_find(): with capture_verbosity() as o: def foo(): seen = [] def not_first(x): if not seen: seen.append(x) return False return x not in seen find( integers(), not_first, settings=settings(verbosity=Verbosity.verbose, max_examples=1000), ) foo() assert "Trying example" in o.getvalue() def test_includes_intermediate_results_in_verbose_mode(): with capture_verbosity() as o: @fails @settings( verbosity=Verbosity.verbose, database=None, derandomize=True, max_examples=100, ) @given(lists(integers(), min_size=1)) def test_foo(x): assert sum(x) < 10000 test_foo() lines = o.getvalue().splitlines() assert len([l for l in lines if "example" in l]) > 2 assert [l for l in lines if "AssertionError" in l] @example(0) @settings(verbosity=Verbosity.quiet) @given(integers()) def test_no_indexerror_in_quiet_mode(x): # Regression tests for https://github.com/HypothesisWorks/hypothesis/issues/2696 # where quiet mode -> no fragments to report -> IndexError accessing first report pass @fails @example(0) @settings(verbosity=Verbosity.quiet, report_multiple_bugs=True) @given(integers()) def test_no_indexerror_in_quiet_mode_report_multiple(x): assert x @fails @example(0) @settings(verbosity=Verbosity.quiet, report_multiple_bugs=False) @given(integers()) def test_no_indexerror_in_quiet_mode_report_one(x): assert x ================================================ FILE: hypothesis-python/tests/crosshair/test_conformance.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from crosshair.core import IgnoreAttempt, NotDeterministic, UnexploredPath from hypothesis_crosshair_provider.crosshair_provider import CrossHairPrimitiveProvider from hypothesis import Verbosity, settings, strategies as st from hypothesis.internal.conjecture.provider_conformance import run_conformance_test def test_provider_conformance_crosshair(): # Hypothesis can in theory pass values of any type to `realize`, # but the default strategy in the conformance test here acts too much like a # fuzzer for crosshair internals here and finds very strange errors. _realize_objects = ( st.integers() | st.floats() | st.booleans() | st.binary() | st.text() ) run_conformance_test( CrossHairPrimitiveProvider, context_manager_exceptions=(IgnoreAttempt, UnexploredPath, NotDeterministic), settings=settings( max_examples=5, stateful_step_count=10, # It's nice to set Verbosity.verbose unconditionally here so we get # more information during rare errors without having to retry. # # Careful, though: it's possible interactions with crosshair differ # under verbosity (eg realizing the symbolic debug messages), so # some errors may only surface with or without verbosity. verbosity=Verbosity.verbose, ), _realize_objects=_realize_objects, ) ================================================ FILE: hypothesis-python/tests/crosshair/test_crosshair.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import crosshair import pytest from hypothesis_crosshair_provider.crosshair_provider import CrossHairPrimitiveProvider from hypothesis import Phase, Verbosity, event, given, settings, strategies as st from hypothesis.database import InMemoryExampleDatabase from hypothesis.internal.conjecture.providers import COLLECTION_DEFAULT_MAX_SIZE from hypothesis.internal.intervalsets import IntervalSet from hypothesis.internal.observability import with_observability_callback from hypothesis.vendor.pretty import pretty from tests.common.utils import capture_observations from tests.conjecture.common import float_constr, integer_constr, string_constr @pytest.mark.parametrize("verbosity", list(Verbosity)) def test_crosshair_works_for_all_verbosities(verbosity): # check that we aren't realizing symbolics early in debug prints and killing # test effectiveness. @given(st.integers()) @settings(backend="crosshair", verbosity=verbosity, database=None) def f(n): assert n != 123456 with pytest.raises(AssertionError): f() @pytest.mark.parametrize("verbosity", list(Verbosity)) def test_crosshair_works_for_all_verbosities_data(verbosity): # data draws have their own print path if verbosity == Verbosity.quiet: pytest.skip("Flaky test, pending fix") @given(st.data()) @settings(backend="crosshair", verbosity=verbosity, database=None) def f(data): n = data.draw(st.integers()) assert n != 123456 with pytest.raises(AssertionError): f() def test_hypothesis_realizes_on_fatal_error(): # BaseException or internal hypothesis failures have a different database save # path. Make sure we realize symbolic values on that path. This test is a bit # of a no-op because we're really relying on our realization validation to # pass here. db = InMemoryExampleDatabase() @given(st.integers()) @settings(database=db, backend="crosshair") def f(n): raise BaseException("marker") with pytest.raises(BaseException, match="marker"): f() def count_choices_for(choice_type, constraints): # returns the number of choices that crosshair makes for this draw, before # hypothesis ever has a chance to interact with it. provider = CrossHairPrimitiveProvider() with provider.per_test_case_context_manager(): assert len(crosshair.statespace.context_statespace().choices_made) == 0 getattr(provider, f"draw_{choice_type}")(**constraints) return len(crosshair.statespace.context_statespace().choices_made) @pytest.mark.parametrize( "strategy, expected_choices", [ (st.integers(), lambda: count_choices_for("integer", integer_constr())), (st.floats(), lambda: count_choices_for("float", float_constr())), ( st.binary(), lambda: count_choices_for( "bytes", {"min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE} ), ), (st.booleans(), lambda: count_choices_for("boolean", {})), ( st.text(), lambda: count_choices_for( "string", string_constr(IntervalSet.from_string("a")) ), ), ], ids=pretty, ) def test_no_path_constraints_are_added_to_symbolic_values(strategy, expected_choices): # check that we don't interact with returned symbolics from the crosshair # provider in a way that would add decisions to crosshair's state space (ie # add path constraints). expected_choices = expected_choices() # skip the first call, which is ChoiceTemplate(type="simplest") with the # hypothesis backend. called = False # limit to one example to prevent crosshair from raising e.g. # BackendCannotProceed(scope="verified") and switching to the hypothesis # provider @given(strategy) @settings( backend="crosshair", database=None, phases={Phase.generate}, max_examples=2 ) def f(value): nonlocal called if not called: called = True return # if this test ever fails, we will replay it without crosshair, in which # case the statespace is None. statespace = crosshair.statespace.optional_context_statespace() assert statespace is not None, "this test failed under crosshair" assert len(statespace.choices_made) == expected_choices f() @pytest.mark.parametrize( "strategy, extra_observability", [ # we add an additional path constraint to ints in to_jsonable. (st.integers(), 1), (st.text(), 0), (st.booleans(), 0), (st.floats(), 0), (st.binary(), 0), ], ) def test_observability_and_verbosity_dont_add_choices(strategy, extra_observability): choices = {} # skip the first call, which is ChoiceTemplate(type="simplest") with the # hypothesis backend. called = False @given(strategy) @settings(backend="crosshair", database=None, max_examples=2) def f_normal(value): nonlocal called if called: choices["normal"] = len( crosshair.statespace.context_statespace().choices_made ) called = True f_normal() called = False @given(strategy) @settings( backend="crosshair", database=None, max_examples=2, verbosity=Verbosity.debug ) def f_verbosity(value): nonlocal called if called: choices["verbosity"] = len( crosshair.statespace.context_statespace().choices_made ) called = True f_verbosity() called = False @given(strategy) @settings(backend="crosshair", database=None, max_examples=2) def f_observability(value): nonlocal called if called: choices["observability"] = len( crosshair.statespace.context_statespace().choices_made ) called = True with capture_observations(): f_observability() assert ( choices["normal"] == (choices["observability"] - extra_observability) == choices["verbosity"] ) def test_realizes_event(): saw_myevent = False def callback(observation): if observation.type != "test_case": return nonlocal saw_myevent # crosshair might raise BackendCannotProceed(verified) during generation, # which never reaches event(). if "myevent" in observation.features: assert isinstance(observation.features["myevent"], int) saw_myevent = True @given(st.integers()) @settings(backend="crosshair", max_examples=5) def test(n): event("myevent", n) with with_observability_callback(callback): test() assert saw_myevent @given(st.integers()) @settings(backend="crosshair") def test_event_with_realization(value): event(value) float(value) ================================================ FILE: hypothesis-python/tests/datetime/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/datetime/test_dateutil_timezones.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime as dt import sys import warnings import pytest from hypothesis import assume, given from hypothesis.errors import FailedHealthCheck, InvalidArgument from hypothesis.strategies import data, datetimes, just, sampled_from, times from hypothesis.strategies._internal.datetime import datetime_does_not_exist from tests.common.debug import assert_all_examples, find_any, minimal from tests.common.utils import Why, fails_with, xfail_on_crosshair with warnings.catch_warnings(): if sys.version_info[:2] >= (3, 12): # Prior to https://github.com/dateutil/dateutil/pull/1285/ warnings.simplefilter("ignore", DeprecationWarning) from dateutil import tz, zoneinfo from hypothesis.extra.dateutil import timezones def test_utc_is_minimal(): assert tz.UTC is minimal(timezones()) def test_can_generate_non_naive_time(): assert minimal(times(timezones=timezones()), lambda d: d.tzinfo).tzinfo == tz.UTC def test_can_generate_non_naive_datetime(): assert ( minimal(datetimes(timezones=timezones()), lambda d: d.tzinfo).tzinfo == tz.UTC ) @given(datetimes(timezones=timezones())) def test_timezone_aware_datetimes_are_timezone_aware(dt): assert dt.tzinfo is not None @given(sampled_from(["min_value", "max_value"]), datetimes(timezones=timezones())) def test_datetime_bounds_must_be_naive(name, val): with pytest.raises(InvalidArgument): datetimes(**{name: val}).validate() def test_timezones_arg_to_datetimes_must_be_search_strategy(): all_timezones = zoneinfo.get_zonefile_instance().zones with pytest.raises(InvalidArgument): datetimes(timezones=all_timezones).validate() @given(times(timezones=timezones())) def test_timezone_aware_times_are_timezone_aware(dt): assert dt.tzinfo is not None def test_can_generate_non_utc(): times(timezones=timezones()).filter( lambda d: assume(d.tzinfo) and d.tzinfo.zone != "UTC" ).validate() @given(sampled_from(["min_value", "max_value"]), times(timezones=timezones())) def test_time_bounds_must_be_naive(name, val): with pytest.raises(InvalidArgument): times(**{name: val}).validate() def test_should_have_correct_ordering(): def offset(timezone): return abs(timezone.utcoffset(dt.datetime(2000, 1, 1))) next_interesting_tz = minimal(timezones(), lambda tz: offset(tz) > dt.timedelta(0)) assert offset(next_interesting_tz) == dt.timedelta(seconds=3600) @given(data(), datetimes(), datetimes()) def test_datetimes_stay_within_naive_bounds(data, lo, hi): if lo > hi: lo, hi = hi, lo out = data.draw(datetimes(lo, hi, timezones=timezones())) assert lo <= out.replace(tzinfo=None) <= hi DAY_WITH_IMAGINARY_HOUR_KWARGS = { # The day of a spring-forward transition; 2am is imaginary "min_value": dt.datetime(2020, 10, 4), "max_value": dt.datetime(2020, 10, 5), "timezones": just(tz.gettz("Australia/Sydney")), } @xfail_on_crosshair(Why.other, strict=False) # fromutc argument must be a datetime @given(datetimes(timezones=timezones()) | datetimes(**DAY_WITH_IMAGINARY_HOUR_KWARGS)) def test_dateutil_exists_our_not_exists_are_inverse(value): assert datetime_does_not_exist(value) == (not tz.datetime_exists(value)) def test_datetimes_can_exclude_imaginary(): find_any( datetimes(**DAY_WITH_IMAGINARY_HOUR_KWARGS, allow_imaginary=True), lambda x: not tz.datetime_exists(x), ) assert_all_examples( datetimes(**DAY_WITH_IMAGINARY_HOUR_KWARGS, allow_imaginary=False), tz.datetime_exists, ) @xfail_on_crosshair(Why.other) @fails_with(FailedHealthCheck) @given( datetimes( max_value=dt.datetime(1, 1, 1, 9), timezones=just(tz.gettz("Australia/Sydney")), allow_imaginary=False, ) ) def test_non_imaginary_datetimes_at_boundary(val): # This is expected to fail because Australia/Sydney is UTC+10, # and the filter logic overflows when checking for round-trips. raise AssertionError ================================================ FILE: hypothesis-python/tests/datetime/test_pytz_timezones.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime as dt import sys import warnings import pytest from hypothesis import assume, given, settings, strategies as st from hypothesis.errors import InvalidArgument, StopTest from hypothesis.strategies import data, datetimes, just, sampled_from, times from hypothesis.strategies._internal.datetime import datetime_does_not_exist from tests.common.debug import assert_all_examples, find_any, minimal from tests.common.utils import Why, xfail_on_crosshair with warnings.catch_warnings(): if sys.version_info[:2] >= (3, 12): # See https://github.com/stub42/pytz/issues/105 and # https://github.com/dateutil/dateutil/pull/1285/ warnings.simplefilter("ignore", DeprecationWarning) import pytz from dateutil.tz import datetime_exists from hypothesis.extra.pytz import timezones def test_utc_is_minimal(): assert pytz.UTC is minimal(timezones()) def test_can_generate_non_naive_time(): assert minimal(times(timezones=timezones()), lambda d: d.tzinfo).tzinfo == pytz.UTC def test_can_generate_non_naive_datetime(): assert ( minimal(datetimes(timezones=timezones()), lambda d: d.tzinfo).tzinfo == pytz.UTC ) @given(datetimes(timezones=timezones())) def test_timezone_aware_datetimes_are_timezone_aware(dt): assert dt.tzinfo is not None @given(sampled_from(["min_value", "max_value"]), datetimes(timezones=timezones())) def test_datetime_bounds_must_be_naive(name, val): with pytest.raises(InvalidArgument): datetimes(**{name: val}).validate() def test_underflow_in_simplify(): # we shouldn't trigger a pytz bug when we're simplifying minimal( datetimes( max_value=dt.datetime.min + dt.timedelta(days=3), timezones=timezones() ), lambda x: x.tzinfo != pytz.UTC, ) def test_overflow_in_simplify(): # we shouldn't trigger a pytz bug when we're simplifying minimal( datetimes( min_value=dt.datetime.max - dt.timedelta(days=3), timezones=timezones() ), lambda x: x.tzinfo != pytz.UTC, ) def test_timezones_arg_to_datetimes_must_be_search_strategy(): with pytest.raises(InvalidArgument): datetimes(timezones=pytz.all_timezones).validate() tz = [pytz.timezone(t) for t in pytz.all_timezones] with pytest.raises(InvalidArgument): datetimes(timezones=tz).validate() @given(times(timezones=timezones())) def test_timezone_aware_times_are_timezone_aware(dt): assert dt.tzinfo is not None def test_can_generate_non_utc(): times(timezones=timezones()).filter( lambda d: assume(d.tzinfo) and d.tzinfo.zone != "UTC" ).validate() @given(sampled_from(["min_value", "max_value"]), times(timezones=timezones())) def test_time_bounds_must_be_naive(name, val): with pytest.raises(InvalidArgument): times(**{name: val}).validate() @pytest.mark.parametrize( "bound", [ {"min_value": dt.datetime.max - dt.timedelta(days=3)}, {"max_value": dt.datetime.min + dt.timedelta(days=3)}, ], ) def test_can_trigger_error_in_draw_near_boundary(bound): found = False # this would be better written with find_any, but I couldn't get rewriting # with st.composite and assuming the event condition to work. # https://github.com/HypothesisWorks/hypothesis/pull/4229#discussion_r1907993831 @given(st.data()) @settings(max_examples=1000) def f(data): try: data.draw(datetimes(**bound, timezones=timezones())) except StopTest: pass if "Failed to draw a datetime" in data.conjecture_data.events.get( "invalid because", "" ): nonlocal found found = True f() assert found @given(data(), datetimes(), datetimes()) def test_datetimes_stay_within_naive_bounds(data, lo, hi): if lo > hi: lo, hi = hi, lo out = data.draw(datetimes(lo, hi, timezones=timezones())) assert lo <= out.replace(tzinfo=None) <= hi @pytest.mark.parametrize( "kw", [ # Ireland uses *negative* offset DST, which means that our sloppy interpretation # of "is_dst=not fold" bypasses the filter for imaginary times. This is basically # unfixable without redesigning pytz per PEP-495, and it's much more likely to be # replaced by dateutil or PEP-615 zoneinfo in the standard library instead. { "min_value": dt.datetime(2019, 3, 31), "max_value": dt.datetime(2019, 4, 1), "timezones": just(pytz.timezone("Europe/Dublin")), }, # The day of a spring-forward transition in Australia; 2am is imaginary # (the common case so an optimistic `is_dst=bool(fold)` also fails the test) { "min_value": dt.datetime(2020, 10, 4), "max_value": dt.datetime(2020, 10, 5), "timezones": just(pytz.timezone("Australia/Sydney")), }, ], ) @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_datetimes_can_exclude_imaginary(kw): # Sanity check: fail unless those days contain an imaginary hour to filter out find_any(datetimes(**kw, allow_imaginary=True), lambda x: not datetime_exists(x)) # Assert that with allow_imaginary=False we only generate existing datetimes. assert_all_examples(datetimes(**kw, allow_imaginary=False), datetime_exists) def test_really_weird_tzinfo_case(): x = dt.datetime(2019, 3, 31, 2, 30, tzinfo=pytz.timezone("Europe/Dublin")) assert x.tzinfo is not x.astimezone(dt.timezone.utc).astimezone(x.tzinfo) # And that weird case exercises the rare branch in our helper: assert datetime_does_not_exist(x) ================================================ FILE: hypothesis-python/tests/datetime/test_zoneinfo_timezones.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import platform import zoneinfo import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument from tests.common.debug import assert_no_examples, find_any, minimal def test_utc_is_minimal(): assert minimal(st.timezones()) is zoneinfo.ZoneInfo("UTC") def test_can_generate_non_utc(): find_any( st.datetimes(timezones=st.timezones()).filter(lambda d: d.tzinfo.key != "UTC") ) @given(st.data(), st.datetimes(), st.datetimes()) def test_datetimes_stay_within_naive_bounds(data, lo, hi): if lo > hi: lo, hi = hi, lo out = data.draw(st.datetimes(lo, hi, timezones=st.timezones())) assert lo <= out.replace(tzinfo=None) <= hi @pytest.mark.parametrize("kwargs", [{"no_cache": 1}]) def test_timezones_argument_validation(kwargs): with pytest.raises(InvalidArgument): st.timezones(**kwargs).validate() @pytest.mark.parametrize( "kwargs", [ # {"allow_alias": 1}, # {"allow_deprecated": 1}, {"allow_prefix": 1}, ], ) def test_timezone_keys_argument_validation(kwargs): with pytest.raises(InvalidArgument): st.timezone_keys(**kwargs).validate() @pytest.mark.xfail(strict=False, reason="newly failing on GitHub Actions") @pytest.mark.skipif(platform.system() != "Linux", reason="platform-specific") def test_can_generate_prefixes_if_allowed_and_available(): """ This is actually kinda fiddly: we may generate timezone keys with the "posix/" or "right/" prefix if-and-only-if they are present on the filesystem. This immediately rules out Windows (which uses the tzdata package instead), along with OSX (which doesn't seem to have prefixed keys). We believe that they are present on at least most Linux distros, but have not done exhaustive testing. It's fine to just patch this test out if it fails - passing in the Hypothesis CI demonstrates that the feature works on *some* systems. """ find_any(st.timezone_keys(), lambda s: s.startswith("posix/")) find_any(st.timezone_keys(), lambda s: s.startswith("right/")) def test_can_disallow_prefixes(): assert_no_examples( st.timezone_keys(allow_prefix=False), lambda s: s.startswith(("posix/", "right/")), ) ================================================ FILE: hypothesis-python/tests/django/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/django/manage.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import sys import warnings from hypothesis import HealthCheck, settings from tests.common.setup import run if __name__ == "__main__": run() settings.register_profile( "default", settings(suppress_health_check=[HealthCheck.too_slow]) ) settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.django.toys.settings") # This triggers a deprecation warning on some older versions of Django # because of its use of the imp module. with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecationWarning) from django.core.management import execute_from_command_line try: from django.utils.deprecation import RemovedInDjango50Warning except ImportError: RemovedInDjango50Warning = () try: from django.utils.deprecation import RemovedInDjango60Warning except ImportError: RemovedInDjango60Warning = () with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RemovedInDjango50Warning) warnings.simplefilter("ignore", category=RemovedInDjango60Warning) execute_from_command_line(sys.argv) ================================================ FILE: hypothesis-python/tests/django/toys/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/django/toys/settings/no_urls.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. urlpatterns = [] ================================================ FILE: hypothesis-python/tests/django/toys/settings/settings.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Django settings for toys project. For more information on this file, see https://docs.djangoproject.com/en/1.7/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.7/ref/settings/ """ import os import django # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "o0zlv@74u4e3s+o0^h$+tlalh&$r(7hbx01g4^h5-3gizj%hub" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True TEMPLATE_DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = ( "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "tests.django.toystore", ) MIDDLEWARE_CLASSES = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.SessionAuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ) ROOT_URLCONF = "tests.django.toys.settings.urls" WSGI_APPLICATION = "tests.django.toys.wsgi.application" DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True if django.VERSION < (5, 0, 0): USE_L10N = True USE_TZ = os.environ.get("HYPOTHESIS_DJANGO_USETZ", "TRUE") == "TRUE" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ STATIC_URL = "/static/" # Added these bits to avoid warnings on Django 2.2 TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ] }, } ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] # Transitional setting until 6.0. See # https://docs.djangoproject.com/en/5.0/ref/forms/fields/#django.forms.URLField.assume_scheme FORMS_URLFIELD_ASSUME_HTTPS = True ================================================ FILE: hypothesis-python/tests/django/toys/settings/settings_no_contrib.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from .settings import * # noqa: F403 # We test django in two contexts: with some default django.contrib apps installed # (which is settings.py), and with no django.contrib apps installed (which is this # file). We set DJANGO_SETTINGS_MODULE in tox to select which settings file we # use during testing. INSTALLED_APPS = ["tests.django.toystore"] ROOT_URLCONF = "tests.django.toys.settings.no_urls" ================================================ FILE: hypothesis-python/tests/django/toys/settings/urls.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from django.contrib import admin from django.urls import include, re_path patterns, namespace, name = admin.site.urls urlpatterns = [ # Examples: # url(r'^$', 'toys.views.home', name='home'), # url(r'^blog/', include('blog.urls')), re_path(r"^admin/", include((patterns, name), namespace=namespace)) ] ================================================ FILE: hypothesis-python/tests/django/toys/wsgi.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """WSGI config for toys project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "toys.settings") application = get_wsgi_application() ================================================ FILE: hypothesis-python/tests/django/toystore/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/django/toystore/admin.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/django/toystore/forms.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from django import forms from django.conf import settings from django.core.validators import ( MaxLengthValidator, MaxValueValidator, MinLengthValidator, MinValueValidator, ) from django.forms import widgets from tests.django.toystore.models import ( Company, CouldBeCharming, Customer, FileFields, ManyNumerics, ManyTimes, OddFields, Store, ) if "django.contrib.auth" in settings.INSTALLED_APPS: from django.contrib.auth.forms import ReadOnlyPasswordHashField, UsernameField class ReprModelForm(forms.ModelForm): def __repr__(self): """I recommend putting this in your form to show the failed cases.""" return f"{self.data!r}\n{self.errors!r}" class ReprForm(forms.Form): def __repr__(self): return f"{self.data!r}\n{self.errors!r}" class CouldBeCharmingForm(ReprModelForm): class Meta: model = CouldBeCharming fields = "__all__" class FileFieldsForm(ReprModelForm): class Meta: model = FileFields fields = "__all__" class CustomerForm(ReprModelForm): class Meta: model = Customer fields = "__all__" class ManyNumericsForm(ReprModelForm): class Meta: model = ManyNumerics fields = "__all__" class ManyTimesForm(ReprModelForm): class Meta: model = ManyTimes fields = "__all__" class OddFieldsForm(ReprModelForm): class Meta: model = OddFields fields = "__all__" class DynamicForm(ReprForm): def __init__(self, field_count=5, **kwargs): super().__init__(**kwargs) for i in range(field_count): field_name = f"field-{i}" self.fields[field_name] = forms.CharField(required=False) class BasicFieldForm(ReprForm): _boolean_required = forms.BooleanField() _boolean = forms.BooleanField(required=False) # This took me too long to figure out... The BooleanField will actually # raise a ValidationError when it receives a value of False. Why they # didn't call it a TrueOnlyField escapes me, but *if* you actually want # to accept both True and False in your BooleanField, make sure you set # `required=False`. This behavior has been hotly contested in the bug # tracker (e.g. https://code.djangoproject.com/ticket/23547), but it # seems that since the tests and documentation are already written # this behavior is Truth. # see the note in the documentation # https://docs.djangoproject.com/en/dev/ref/forms/fields/#booleanfield _char_required = forms.CharField(required=True) _char = forms.CharField(required=False) _decimal = forms.DecimalField(max_digits=8, decimal_places=3) _float = forms.FloatField() _integer = forms.IntegerField() _null_boolean = forms.NullBooleanField() class TemporalFieldForm(ReprForm): _date = forms.DateField() _date_time = forms.DateTimeField() _duration = forms.DurationField() _time = forms.TimeField() _split_date_time = forms.SplitDateTimeField() class WithValidatorsForm(ReprForm): num_validators = (MinValueValidator(1), MaxValueValidator(5)) _int_one_to_five = forms.IntegerField(validators=num_validators) _decimal_one_to_five = forms.FloatField(validators=num_validators) _float_one_to_five = forms.FloatField(validators=num_validators) len_validators = (MinLengthValidator(5), MaxLengthValidator(10)) _string_five_to_ten = forms.CharField(validators=len_validators) class EmailFieldForm(ReprForm): _email = forms.EmailField() class SlugFieldForm(ReprForm): _slug = forms.SlugField() class URLFieldForm(ReprForm): _url = forms.URLField() class RegexFieldForm(ReprForm): _regex = forms.RegexField(regex="[A-Z]{3}\\.[a-z]{4}") class UUIDFieldForm(ReprForm): _uuid = forms.UUIDField() class ChoiceFieldForm(ReprForm): _choice = forms.ChoiceField( choices=(("cola", "Cola"), ("tea", "Tea"), ("water", "Water")) ) _multiple = forms.MultipleChoiceField( choices=(("cola", "Cola"), ("tea", "Tea"), ("water", "Water")) ) _typed = forms.TypedChoiceField( choices=(("1", "one"), ("2", "two"), ("3", "three"), ("4", "four")), coerce=int, empty_value=0, ) _typed_multiple = forms.TypedMultipleChoiceField( choices=(("1", "one"), ("2", "two"), ("3", "three"), ("4", "four")), coerce=int, empty_value=0, ) class InternetProtocolForm(ReprForm): _ip_both = forms.GenericIPAddressField() _ip_v4 = forms.GenericIPAddressField(protocol="IPv4") _ip_v6 = forms.GenericIPAddressField(protocol="IPv6") class BroadBooleanInput(widgets.CheckboxInput): """Basically pulled directly from the Django CheckboxInput. I added some stuff to ``values`` """ def value_from_datadict(self, data, files, name): if name not in data: return False value = data.get(name) # Translate true and false strings to boolean values. values = {"true": True, "false": False, "0": False, "1": True} if isinstance(value, str): value = values.get(value.lower(), value) return bool(value) class MultiCheckboxWidget(widgets.MultiWidget): def __init__(self, subfield_count=12, **kwargs): _widgets = [BroadBooleanInput()] * subfield_count super().__init__(_widgets, **kwargs) def decompress(self, value): values = [] for _value in value.split("::"): if _value in ("0", "", "False", 0, None, False): values.append(False) else: values.append(True) return values class BroadBooleanField(forms.BooleanField): pass class MultiBooleanField(forms.MultiValueField): def __init__(self, subfield_count=12, **kwargs): subfields = [BroadBooleanField()] * subfield_count widget = MultiCheckboxWidget(subfield_count=subfield_count) super().__init__(fields=subfields, widget=widget) def compress(self, values): return "::".join(str(x) for x in values) class ManyMultiValueForm(ReprForm): def __init__(self, subfield_count=12, **kwargs): super().__init__(**kwargs) self.fields["mv_field"] = MultiBooleanField(subfield_count=subfield_count) class ShortStringForm(ReprForm): _not_too_long = forms.CharField(max_length=20, required=False) if "django.contrib.auth" in settings.INSTALLED_APPS: class UsernameForm(ReprForm): username = UsernameField() class ReadOnlyPasswordHashFieldForm(ReprForm): password = ReadOnlyPasswordHashField() else: UsernameForm = None ReadOnlyPasswordHashFieldForm = None class StoreForm(ReprModelForm): company = forms.ModelChoiceField(queryset=Company.objects.order_by("name")) class Meta: model = Store fields = "__all__" class MultipleCompaniesForm(ReprForm): companies = forms.ModelMultipleChoiceField( queryset=Company.objects.order_by("name") ) ================================================ FILE: hypothesis-python/tests/django/toystore/models.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import django from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models class Company(models.Model): name = models.CharField(max_length=100, unique=True) class Store(models.Model): name = models.CharField(max_length=100, unique=True) company = models.ForeignKey(Company, null=False, on_delete=models.CASCADE) class Car(models.Model): # See https://github.com/HypothesisWorks/hypothesis/issues/2369 model = models.CharField(max_length=100, unique=True) class CharmField(models.Field): def db_type(self, connection): return "char(1)" class CustomishField(models.Field): def db_type(self, connection): return "char(1)" class Customish(models.Model): customish = CustomishField() class Customer(models.Model): name = models.CharField(max_length=100, unique=True) email = models.EmailField(max_length=100, unique=True) gender = models.CharField(max_length=50, null=True) # noqa # avoid nullable strs age = models.IntegerField() birthday = models.DateTimeField() class Charming(models.Model): charm = CharmField() class CouldBeCharming(models.Model): charm = CharmField(null=True) class SelfLoop(models.Model): me = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) class LoopA(models.Model): b = models.ForeignKey("LoopB", null=False, on_delete=models.CASCADE) class LoopB(models.Model): a = models.ForeignKey("LoopA", null=True, on_delete=models.SET_NULL) class ManyNumerics(models.Model): i1 = models.IntegerField() i2 = models.SmallIntegerField() i3 = models.BigIntegerField() p1 = models.PositiveIntegerField() p2 = models.PositiveSmallIntegerField() d = models.DecimalField(decimal_places=2, max_digits=5) class ManyTimes(models.Model): time = models.TimeField() date = models.DateField() duration = models.DurationField() class OddFields(models.Model): uuid = models.UUIDField() slug = models.SlugField() url = models.URLField() ipv4 = models.GenericIPAddressField(protocol="IPv4") ipv6 = models.GenericIPAddressField(protocol="IPv6") class CustomishDefault(models.Model): customish = CustomishField(default="b") class FileFields(models.Model): file1 = models.FileField() class MandatoryComputed(models.Model): name = models.CharField(max_length=100, unique=True) company = models.ForeignKey(Company, null=False, on_delete=models.CASCADE) def __init__(self, **kw): if "company" in kw: raise RuntimeError cname = kw["name"] + "_company" kw["company"] = Company.objects.create(name=cname) super().__init__(**kw) def validate_even(value): if value % 2 != 0: raise ValidationError("") class RestrictedFields(models.Model): text_field_4 = models.TextField(max_length=4, blank=True) char_field_4 = models.CharField(max_length=4, blank=True) choice_field_text = models.TextField(choices=(("foo", "Foo"), ("bar", "Bar"))) choice_field_int = models.IntegerField(choices=((1, "First"), (2, "Second"))) null_choice_field_int = models.IntegerField( choices=((1, "First"), (2, "Second")), null=True, blank=True ) choice_field_grouped = models.TextField( choices=( ("Audio", (("vinyl", "Vinyl"), ("cd", "CD"))), ("Video", (("vhs", "VHS Tape"), ("dvd", "DVD"))), ("unknown", "Unknown"), ) ) even_number_field = models.IntegerField(validators=[validate_even]) non_blank_text_field = models.TextField(blank=False) class SelfModifyingField(models.IntegerField): def pre_save(self, model_instance, add): value = getattr(model_instance, self.attname) value += 1 setattr(model_instance, self.attname, value) return value class CompanyExtension(models.Model): company = models.OneToOneField(Company, primary_key=True, on_delete=models.CASCADE) self_modifying = SelfModifyingField() class UserSpecifiedAutoId(models.Model): my_id = models.AutoField(primary_key=True) if django.VERSION >= (5, 0, 0): import math class Pizza(models.Model): AREA = math.pi * models.F("radius") ** 2 radius = models.IntegerField(validators=[MinValueValidator(1)]) slices = models.PositiveIntegerField(validators=[MinValueValidator(2)]) total_area = models.GeneratedField( expression=AREA, output_field=models.FloatField(), db_persist=True, ) slice_area = models.GeneratedField( expression=AREA / models.F("slices"), output_field=models.FloatField(), db_persist=False, ) ================================================ FILE: hypothesis-python/tests/django/toystore/test_basic_configuration.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from unittest import TestCase as VanillaTestCase import pytest from django.db import IntegrityError from django.test import TestCase as DjangoTestCase from hypothesis import HealthCheck, Verbosity, given, settings from hypothesis.errors import InvalidArgument from hypothesis.extra.django import SimpleTestCase, TestCase, TransactionTestCase from hypothesis.internal.compat import GRAALPY, PYPY from hypothesis.strategies import integers from tests.django.toystore.models import Company class SomeStuff: @settings( suppress_health_check=[HealthCheck.too_slow, HealthCheck.differing_executors] ) @given(integers()) def test_is_blank_slate(self, unused): Company.objects.create(name="MickeyCo") def test_normal_test_1(self): Company.objects.create(name="MickeyCo") def test_normal_test_2(self): Company.objects.create(name="MickeyCo") class TestConstraintsWithTransactions(SomeStuff, TestCase): pass if not (PYPY or GRAALPY): # xfail # This is excessively slow in general, but particularly on pypy. We just # disable it altogether there as it's a niche case. class TestConstraintsWithoutTransactions(SomeStuff, TransactionTestCase): pass class TestWorkflow(VanillaTestCase): def test_does_not_break_later_tests(self): def break_the_db(i): Company.objects.create(name="MickeyCo") Company.objects.create(name="MickeyCo") class LocalTest(TestCase): @given(integers().map(break_the_db)) @settings( suppress_health_check=list(HealthCheck), verbosity=Verbosity.quiet ) def test_does_not_break_other_things(self, unused): pass def test_normal_test_1(self): Company.objects.create(name="MickeyCo") t = LocalTest("test_normal_test_1") try: t.test_does_not_break_other_things() except IntegrityError: pass t.test_normal_test_1() def test_given_needs_hypothesis_test_case(self): class LocalTest(DjangoTestCase): @given(integers()) def tst(self, i): raise AssertionError("InvalidArgument should be raised in @given") with pytest.raises(InvalidArgument): LocalTest("tst").tst() class TestSimple(SimpleTestCase): @given(integers()) def test_that_doesnt_need_db(self, z: int): company = Company(name="Company-" + str(z)) assert company.name.endswith(str(z)) ================================================ FILE: hypothesis-python/tests/django/toystore/test_given_forms.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from unittest import skipIf from django import forms from django.conf import settings from hypothesis import assume, given from hypothesis.extra.django import ( TestCase, from_field, from_form, register_field_strategy, ) from hypothesis.strategies import booleans, sampled_from from tests.django.toystore.forms import ( BasicFieldForm, BroadBooleanField, ChoiceFieldForm, CustomerForm, DynamicForm, EmailFieldForm, FileFieldsForm, InternetProtocolForm, ManyMultiValueForm, ManyNumericsForm, ManyTimesForm, MultipleCompaniesForm, OddFieldsForm, RegexFieldForm, ShortStringForm, SlugFieldForm, StoreForm, TemporalFieldForm, URLFieldForm, UsernameForm, UUIDFieldForm, WithValidatorsForm, ) from tests.django.toystore.models import Company register_field_strategy( BroadBooleanField, booleans() | sampled_from(["1", "0", "True", "False"]) ) has_contrib_auth = "django.contrib.auth" in settings.INSTALLED_APPS class TestGetsBasicForms(TestCase): @given(from_form(CustomerForm)) def test_valid_customer(self, customer_form): self.assertTrue(customer_form.is_valid()) @given(from_form(ManyNumericsForm)) def test_valid_numerics(self, numerics_form): self.assertTrue(numerics_form.is_valid()) @given(from_form(ManyTimesForm)) def test_valid_times(self, times_form): self.assertTrue(times_form.is_valid()) @given(from_form(OddFieldsForm)) def test_valid_odd_fields(self, odd_form): self.assertTrue(odd_form.is_valid()) def test_dynamic_form(self): for field_count in range(2, 7): @given(from_form(DynamicForm, form_kwargs={"field_count": field_count})) def _test(dynamic_form): self.assertTrue(dynamic_form.is_valid()) _test() @given(from_form(BasicFieldForm)) def test_basic_fields_form(self, basic_field_form): self.assertTrue(basic_field_form.is_valid()) @given(from_form(TemporalFieldForm)) def test_temporal_fields_form(self, time_field_form): self.assertTrue(time_field_form.is_valid()) @given(from_form(EmailFieldForm)) def test_email_field_form(self, email_field_form): self.assertTrue(email_field_form.is_valid()) @given(from_form(SlugFieldForm)) def test_slug_field_form(self, slug_field_form): self.assertTrue(slug_field_form.is_valid()) @given(from_form(URLFieldForm)) def test_url_field_form(self, url_field_form): self.assertTrue(url_field_form.is_valid()) @given(from_form(RegexFieldForm)) def test_regex_field_form(self, regex_field_form): self.assertTrue(regex_field_form.is_valid()) @given(from_form(UUIDFieldForm)) def test_uuid_field_form(self, uuid_field_form): self.assertTrue(uuid_field_form.is_valid()) @given(from_form(ChoiceFieldForm)) def test_choice_fields_form(self, choice_field_form): self.assertTrue(choice_field_form.is_valid()) @given(from_form(InternetProtocolForm)) def test_ip_fields_form(self, ip_field_form): self.assertTrue(ip_field_form.is_valid()) @given(from_form(ManyMultiValueForm, form_kwargs={"subfield_count": 2})) def test_many_values_in_multi_value_field(self, many_multi_value_form): self.assertTrue(many_multi_value_form.is_valid()) @given(from_form(ManyMultiValueForm, form_kwargs={"subfield_count": 105})) def test_excessive_values_in_multi_value_field(self, excessive_form): self.assertTrue(excessive_form.is_valid()) @given(from_form(ShortStringForm)) def test_short_string_form(self, short_string_form): self.assertTrue(short_string_form.is_valid()) @given(from_form(WithValidatorsForm)) def test_tight_validators_form(self, x): self.assertTrue(1 <= x.data["_int_one_to_five"] <= 5) self.assertTrue(1 <= x.data["_decimal_one_to_five"] <= 5) self.assertTrue(1 <= x.data["_float_one_to_five"] <= 5) self.assertTrue(5 <= len(x.data["_string_five_to_ten"]) <= 10) @given(from_form(FileFieldsForm)) def test_file_fields_form(self, x): assert x.is_valid() # form.data is empty, and form.files has one entry: file1 self.assertFalse(x.data) self.assertTrue(set(x.files.keys()) == {"file1"}) self.assertTrue(x.files["file1"]) @skipIf(not has_contrib_auth, "contrib.auth not installed") @given(from_form(UsernameForm)) def test_username_form(self, username_form): self.assertTrue(username_form.is_valid()) @skipIf(not has_contrib_auth, "contrib.auth not installed") @given(from_form(UsernameForm)) def test_read_only_password_hash_field_form(self, password_form): self.assertTrue(password_form.is_valid()) class TestFormsWithModelChoices(TestCase): @classmethod def setUpTestData(cls): super().setUpTestData() # Set up example Company records to use as choices for # Store.company. These must exist before creating a strategy # for the ModelChoiceField. cls.company_names = ("Bill's Flowers", "Jane's Sporting Goods") for name in cls.company_names: Company.objects.create(name=name) @given( choice=from_field( forms.ModelChoiceField(queryset=Company.objects.order_by("name")) ) ) def test_from_model_choices_field(self, choice): assume(choice != "") # Skip the empty choice. self.assertIsInstance(choice, int) Company.objects.get(id=choice) @given( choice=from_field( forms.ModelChoiceField( queryset=Company.objects.order_by("name"), empty_label=None ) ) ) def test_from_model_choices_field_no_empty_choice(self, choice): Company.objects.get(id=choice) @given(choice=from_field(forms.ModelChoiceField(queryset=Company.objects.none()))) def test_from_model_choices_field_empty(self, choice): self.assertEqual(choice, "") @given(form=from_form(StoreForm)) def test_store_form_valid(self, form): assume(form.data["company"]) self.assertTrue(form.is_valid()) @given( choice=from_field( forms.ModelMultipleChoiceField(queryset=Company.objects.order_by("name")) ) ) def test_from_model_multiple_choices_field(self, choice): n_choices = len(choice) self.assertEqual(n_choices, len(set(choice))) self.assertEqual(n_choices, Company.objects.filter(pk__in=choice).count()) @given(form=from_form(MultipleCompaniesForm)) def test_multiple_companies_form_valid(self, form): self.assertTrue(form.is_valid()) ================================================ FILE: hypothesis-python/tests/django/toystore/test_given_models.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import datetime as dt from unittest import skipIf from uuid import UUID import django from django.conf import settings as django_settings from hypothesis import HealthCheck, assume, given, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra.django import ( TestCase, TransactionTestCase, from_model, register_field_strategy, ) from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.strategies import just, lists if "django.contrib.auth" in django_settings.INSTALLED_APPS: from django.contrib.auth.models import User else: User = None from tests.common.debug import check_can_generate_examples from tests.django.toystore.models import ( Car, Company, CompanyExtension, CouldBeCharming, Customer, Customish, CustomishDefault, CustomishField, MandatoryComputed, ManyNumerics, ManyTimes, OddFields, RestrictedFields, SelfLoop, Store, UserSpecifiedAutoId, ) register_field_strategy(CustomishField, just("a")) class TestGetsBasicModels(TestCase): @given(from_model(Company)) def test_is_company(self, company): self.assertIsInstance(company, Company) self.assertIsNotNone(company.pk) @given(from_model(Store, company=from_model(Company))) def test_can_get_a_store(self, store): assert store.company.pk @given(lists(from_model(Company))) def test_can_get_multiple_models_with_unique_field(self, companies): assume(len(companies) > 1) for c in companies: self.assertIsNotNone(c.pk) self.assertEqual( len({c.pk for c in companies}), len({c.name for c in companies}) ) @settings(suppress_health_check=[HealthCheck.too_slow]) @given(from_model(Customer)) def test_is_customer(self, customer): self.assertIsInstance(customer, Customer) self.assertIsNotNone(customer.pk) self.assertIsNotNone(customer.email) @settings(suppress_health_check=[HealthCheck.too_slow]) @given(from_model(Customer)) def test_tz_presence(self, customer): if django_settings.USE_TZ: self.assertIsNotNone(customer.birthday.tzinfo) else: self.assertIsNone(customer.birthday.tzinfo) @given(from_model(CouldBeCharming)) def test_is_not_charming(self, not_charming): self.assertIsInstance(not_charming, CouldBeCharming) self.assertIsNotNone(not_charming.pk) self.assertIsNone(not_charming.charm) @given(from_model(SelfLoop)) def test_sl(self, sl): self.assertIsNone(sl.me) @given(lists(from_model(ManyNumerics))) def test_no_overflow_in_integer(self, manyints): pass @given(from_model(Customish)) def test_custom_field(self, x): assert x.customish == "a" def test_mandatory_fields_are_mandatory(self): with self.assertRaises(InvalidArgument): check_can_generate_examples(from_model(Store)) def test_mandatory_computed_fields_are_mandatory(self): with self.assertRaises(InvalidArgument): check_can_generate_examples(from_model(MandatoryComputed)) def test_mandatory_computed_fields_may_not_be_provided(self): with self.assertRaises(RuntimeError): check_can_generate_examples( from_model(MandatoryComputed, company=from_model(Company)) ) @given(from_model(CustomishDefault, customish=...)) def test_customish_default_overridden_by_infer(self, x): assert x.customish == "a" @given(from_model(CustomishDefault, customish=...)) def test_customish_infer_uses_registered_instead_of_default(self, x): assert x.customish == "a" @given(from_model(OddFields)) def test_odd_fields(self, x): assert isinstance(x.uuid, UUID) assert isinstance(x.slug, str) assert " " not in x.slug assert isinstance(x.ipv4, str) assert len(x.ipv4.split(".")) == 4 assert all(int(i) in range(256) for i in x.ipv4.split(".")) assert isinstance(x.ipv6, str) assert set(x.ipv6).issubset(set("0123456789abcdefABCDEF:.")) @given(from_model(ManyTimes)) def test_time_fields(self, x): assert isinstance(x.time, dt.time) assert isinstance(x.date, dt.date) assert isinstance(x.duration, dt.timedelta) @given(from_model(Company)) def test_no_null_in_charfield(self, x): # regression test for #1045. Company just has a convenient CharField. assert "\x00" not in x.name @given(st.data()) def test_foreign_key_primary(self, data): # Regression test for #1307 company_strategy = from_model(Company, name=just("test")) strategy = from_model( CompanyExtension, company=company_strategy, self_modifying=just(2) ) data.draw(strategy) # Draw again with the same choice sequence. This will cause a duplicate # primary key. d = ConjectureData.for_choices(data.conjecture_data.choices) d.draw(strategy) assert CompanyExtension.objects.all().count() == 1 class TestsNeedingRollback(TransactionTestCase): def test_can_get_examples(self): for _ in range(200): check_can_generate_examples(from_model(Company)) class TestRestrictedFields(TestCase): @given(from_model(RestrictedFields)) def test_constructs_valid_instance(self, instance): self.assertIsInstance(instance, RestrictedFields) instance.full_clean() self.assertLessEqual(len(instance.text_field_4), 4) self.assertLessEqual(len(instance.char_field_4), 4) self.assertIn(instance.choice_field_text, ("foo", "bar")) self.assertIn(instance.choice_field_int, (1, 2)) self.assertIn(instance.null_choice_field_int, (1, 2, None)) self.assertEqual( instance.choice_field_grouped, instance.choice_field_grouped.lower() ) self.assertEqual(instance.even_number_field % 2, 0) self.assertTrue(instance.non_blank_text_field) @skipIf(User is None, "contrib.auth not installed") class TestValidatorInference(TestCase): @given(from_model(User)) def test_user_issue_1112_regression(self, user): assert user.username class TestPosOnlyArg(TestCase): @given(from_model(Car)) def test_user_issue_2369_regression(self, val): pass def test_from_model_signature(self): self.assertRaises(TypeError, from_model) self.assertRaises(TypeError, from_model, Car, None) self.assertRaises(TypeError, from_model, model=Customer) class TestUserSpecifiedAutoId(TestCase): @given(from_model(UserSpecifiedAutoId)) def test_user_specified_auto_id(self, user_specified_auto_id): self.assertIsInstance(user_specified_auto_id, UserSpecifiedAutoId) self.assertIsNotNone(user_specified_auto_id.pk) if django.VERSION >= (5, 0, 0): from tests.django.toystore.models import Pizza class TestModelWithGeneratedField(TestCase): @given(from_model(Pizza)) def test_create_pizza(self, pizza): """ Strategies are not inferred for GeneratedField. """ # Check we generate valid objects. pizza.full_clean() # Refresh the instance from the database to make sure the # generated fields are populated correctly. pizza.refresh_from_db() # Check the expected types of the generated fields. self.assertIsInstance(pizza.slice_area, float) self.assertIsInstance(pizza.total_area, float) ================================================ FILE: hypothesis-python/tests/django/toystore/views.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/dpcontracts/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/dpcontracts/test_contracts.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from dpcontracts import require from hypothesis import given from hypothesis.errors import InvalidArgument from hypothesis.extra.dpcontracts import fulfill from hypothesis.internal.conjecture.utils import identity from hypothesis.strategies import builds, integers @require("division is undefined for zero", lambda args: args.n != 0) def invert(n): return 1 / n @given(builds(fulfill(invert), integers())) def test_contract_filter_builds(x): assert -1 <= x <= 1 @given(integers()) def test_contract_filter_inline(n): assert -1 <= fulfill(invert)(n) <= 1 @pytest.mark.parametrize("f", [int, identity, lambda x: None]) def test_no_vacuous_fulfill(f): with pytest.raises(InvalidArgument): fulfill(f) ================================================ FILE: hypothesis-python/tests/ghostwriter/example_code/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/ghostwriter/example_code/future_annotations.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations import collections.abc class CustomClass: def __init__(self, number: int) -> None: self.number = number def add_custom_classes(c1: CustomClass, c2: CustomClass | None = None) -> CustomClass: if c2 is None: return CustomClass(c1.number) return CustomClass(c1.number + c2.number) def merge_dicts( map1: collections.abc.Mapping[str, int], map2: collections.abc.Mapping[str, int] ) -> collections.abc.Mapping[str, int]: return {**map1, **map2} def invalid_types(attr1: int, attr2: UnknownClass, attr3: str) -> None: # noqa: F821 pass ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/add_custom_classes.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import example_code.future_annotations import typing from example_code.future_annotations import CustomClass from hypothesis import given, strategies as st @given( c1=st.builds(CustomClass, number=st.integers()), c2=st.one_of(st.none(), st.builds(CustomClass, number=st.integers())), ) def test_fuzz_add_custom_classes( c1: example_code.future_annotations.CustomClass, c2: typing.Union[example_code.future_annotations.CustomClass, None], ) -> None: example_code.future_annotations.add_custom_classes(c1=c1, c2=c2) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/addition_op_magic.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output from hypothesis import given, strategies as st add_operands = st.floats() @given(a=add_operands, b=add_operands, c=add_operands) def test_associative_binary_operation_add(a: float, b: float, c) -> None: left = test_expected_output.add(a=a, b=test_expected_output.add(a=b, b=c)) right = test_expected_output.add(a=test_expected_output.add(a=a, b=b), b=c) assert left == right, (left, right) @given(a=add_operands, b=add_operands) def test_commutative_binary_operation_add(a: float, b: float) -> None: left = test_expected_output.add(a=a, b=b) right = test_expected_output.add(a=b, b=a) assert left == right, (left, right) @given(a=add_operands) def test_identity_binary_operation_add(a: float) -> None: identity = 0.0 assert a == test_expected_output.add(a=a, b=identity) assert a == test_expected_output.add(a=identity, b=a) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/addition_op_multimagic.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import numpy import operator import test_expected_output from hypothesis import given, strategies as st @given(a=st.floats(), b=st.floats()) def test_equivalent_add_add_add(a: float, b: float) -> None: result_add_numpy = numpy.add(a, b) result_add_operator = operator.add(a, b) result_add_test_expected_output = test_expected_output.add(a=a, b=b) assert result_add_numpy == result_add_operator, ( result_add_numpy, result_add_operator, ) assert result_add_numpy == result_add_test_expected_output, ( result_add_numpy, result_add_test_expected_output, ) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/base64_magic.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import base64 from hypothesis import given, strategies as st # TODO: replace st.nothing() with appropriate strategies @given( adobe=st.booleans(), b=st.nothing(), foldspaces=st.booleans(), ignorechars=st.just(b" \t\n\r\x0b"), pad=st.booleans(), wrapcol=st.just(0), ) def test_roundtrip_a85encode_a85decode(adobe, b, foldspaces, ignorechars, pad, wrapcol): value0 = base64.a85encode( b=b, foldspaces=foldspaces, wrapcol=wrapcol, pad=pad, adobe=adobe ) value1 = base64.a85decode( b=value0, foldspaces=foldspaces, adobe=adobe, ignorechars=ignorechars ) assert b == value1, (b, value1) @given(casefold=st.booleans(), s=st.nothing()) def test_roundtrip_b16encode_b16decode(casefold, s): value0 = base64.b16encode(s=s) value1 = base64.b16decode(s=value0, casefold=casefold) assert s == value1, (s, value1) @given(casefold=st.booleans(), map01=st.none(), s=st.nothing()) def test_roundtrip_b32encode_b32decode(casefold, map01, s): value0 = base64.b32encode(s=s) value1 = base64.b32decode(s=value0, casefold=casefold, map01=map01) assert s == value1, (s, value1) @given(altchars=st.none(), s=st.nothing(), validate=st.booleans()) def test_roundtrip_b64encode_b64decode(altchars, s, validate): value0 = base64.b64encode(s=s, altchars=altchars) value1 = base64.b64decode(s=value0, altchars=altchars, validate=validate) assert s == value1, (s, value1) @given(b=st.nothing(), pad=st.booleans()) def test_roundtrip_b85encode_b85decode(b, pad): value0 = base64.b85encode(b=b, pad=pad) value1 = base64.b85decode(b=value0) assert b == value1, (b, value1) @given(input=st.nothing(), output=st.nothing()) def test_roundtrip_encode_decode(input, output): value0 = base64.encode(input=input, output=output) value1 = base64.decode(input=value0, output=output) assert input == value1, (input, value1) @given(s=st.nothing()) def test_roundtrip_encodebytes_decodebytes(s): value0 = base64.encodebytes(s=s) value1 = base64.decodebytes(s=value0) assert s == value1, (s, value1) @given(s=st.nothing()) def test_roundtrip_standard_b64encode_standard_b64decode(s): value0 = base64.standard_b64encode(s=s) value1 = base64.standard_b64decode(s=value0) assert s == value1, (s, value1) @given(s=st.nothing()) def test_roundtrip_urlsafe_b64encode_urlsafe_b64decode(s): value0 = base64.urlsafe_b64encode(s=s) value1 = base64.urlsafe_b64decode(s=value0) assert s == value1, (s, value1) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/division_binop_error_handler.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output from hypothesis import given, strategies as st divide_operands = st.integers() @given(a=divide_operands, b=divide_operands, c=divide_operands) def test_associative_binary_operation_divide(a: int, b: int, c) -> None: left = test_expected_output.divide(a=a, b=test_expected_output.divide(a=b, b=c)) right = test_expected_output.divide(a=test_expected_output.divide(a=a, b=b), b=c) assert left == right, (left, right) @given(a=divide_operands, b=divide_operands) def test_commutative_binary_operation_divide(a: int, b: int) -> None: left = test_expected_output.divide(a=a, b=b) right = test_expected_output.divide(a=b, b=a) assert left == right, (left, right) @given(a=divide_operands) def test_identity_binary_operation_divide(a: int) -> None: identity = 1 assert a == test_expected_output.divide(a=a, b=identity) assert a == test_expected_output.divide(a=identity, b=a) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/division_fuzz_error_handler.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output from hypothesis import given, reject, strategies as st @given(a=st.integers(), b=st.integers()) def test_fuzz_divide(a: int, b: int) -> None: try: test_expected_output.divide(a=a, b=b) except ZeroDivisionError: reject() ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/division_operator.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy truediv_operands = st.nothing() @given(a=truediv_operands) def test_identity_binary_operation_truediv(a): identity = "identity element here" assert a == operator.truediv(a, identity) assert a == operator.truediv(identity, a) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/division_operator_with_annotations.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy truediv_operands = st.nothing() @given(a=truediv_operands) def test_identity_binary_operation_truediv(a) -> None: identity = "identity element here" assert a == operator.truediv(a, identity) assert a == operator.truediv(identity, a) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_arithmeticerror_handler.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator import test_expected_output from hypothesis import given, reject, strategies as st @given(a=st.integers(), b=st.integers()) def test_roundtrip_divide_mul(a: int, b: int) -> None: try: value0 = test_expected_output.divide(a=a, b=b) value1 = operator.mul(value0, b) except ArithmeticError: reject() assert a == value1, (a, value1) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator import test_expected_output from hypothesis import given, reject, strategies as st @given(a=st.integers(), b=st.integers()) def test_roundtrip_divide_mul(a: int, b: int) -> None: try: value0 = test_expected_output.divide(a=a, b=b) except ZeroDivisionError: reject() value1 = operator.mul(value0, b) assert a == value1, (a, value1) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler_without_annotations.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator import test_expected_output from hypothesis import given, reject, strategies as st @given(a=st.integers(), b=st.integers()) def test_roundtrip_divide_mul(a, b): try: value0 = test_expected_output.divide(a=a, b=b) except ZeroDivisionError: reject() value1 = operator.mul(value0, b) assert a == value1, (a, value1) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_typeerror_handler.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator import test_expected_output from hypothesis import given, reject, strategies as st @given(a=st.integers(), b=st.integers()) def test_roundtrip_divide_mul(a: int, b: int) -> None: try: try: value0 = test_expected_output.divide(a=a, b=b) except ZeroDivisionError: reject() value1 = operator.mul(value0, b) except TypeError: reject() assert a == value1, (a, value1) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/eval_equivalent.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import ast from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy @given( globals=st.none(), locals=st.none(), node_or_string=st.text(), source=st.nothing() ) def test_equivalent_eval_literal_eval(globals, locals, node_or_string, source): result_eval = eval(source, globals, locals) result_literal_eval = ast.literal_eval(node_or_string=node_or_string) assert result_eval == result_literal_eval, (result_eval, result_literal_eval) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/fuzz_classmethod.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output from hypothesis import given, strategies as st @given(arg=st.integers()) def test_fuzz_A_Class_a_classmethod(arg: int) -> None: test_expected_output.A_Class.a_classmethod(arg=arg) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/fuzz_sorted.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. from hypothesis import given, strategies as st @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_fuzz_sorted(iterable, key, reverse): sorted(iterable, key=key, reverse=reverse) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/fuzz_sorted_with_annotations.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. from hypothesis import given, strategies as st @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_fuzz_sorted(iterable, key, reverse) -> None: sorted(iterable, key=key, reverse=reverse) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/fuzz_staticmethod.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output from hypothesis import given, strategies as st @given(arg=st.integers()) def test_fuzz_A_Class_a_staticmethod(arg: int) -> None: test_expected_output.A_Class.a_staticmethod(arg=arg) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/fuzz_ufunc.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import numpy from hypothesis import given, strategies as st # TODO: replace st.nothing() with appropriate strategies @given(a=st.nothing(), b=st.nothing()) def test_fuzz_add(a, b): numpy.add(a, b) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/fuzz_with_docstring.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output from hypothesis import given, strategies as st @given( a=st.lists(st.integers()), b=st.one_of(st.none(), st.builds(list), st.builds(tuple)), c=st.sampled_from(["foo", "bar", None]), d=st.just(int), e=st.just(lambda x: f"xx{x}xx"), ) def test_fuzz_with_docstring(a, b, c, d, e): test_expected_output.with_docstring(a=a, b=b, c=c, d=d, e=e) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import collections.abc import datetime import hypothesis import random import typing from collections.abc import Hashable from hypothesis import given, settings, strategies as st @given(condition=st.from_type(object)) def test_fuzz_assume(condition: object) -> None: hypothesis.assume(condition=condition) @given(value=st.text(), payload=st.one_of(st.floats(), st.integers(), st.text())) def test_fuzz_event(value: str, payload: typing.Union[str, int, float]) -> None: hypothesis.event(value=value, payload=payload) @given( specifier=st.from_type(st.SearchStrategy), condition=st.functions(like=lambda *a, **k: None, returns=st.booleans()), settings=st.from_type(typing.Optional[hypothesis.settings]), random=st.one_of(st.none(), st.randoms()), database_key=st.one_of(st.none(), st.binary()), ) def test_fuzz_find( specifier: st.SearchStrategy, condition: collections.abc.Callable[[typing.Any], bool], settings: typing.Union[hypothesis.settings, None], random: typing.Union[random.Random, None], database_key: typing.Union[bytes, None], ) -> None: hypothesis.find( specifier=specifier, condition=condition, settings=settings, random=random, database_key=database_key, ) @given(f=st.from_type(object)) def test_fuzz_is_hypothesis_test(f: object) -> None: hypothesis.is_hypothesis_test(f=f) @given(value=st.from_type(object)) def test_fuzz_note(value: object) -> None: hypothesis.note(value=value) @given(r=st.randoms()) def test_fuzz_register_random(r: random.Random) -> None: hypothesis.register_random(r=r) @given(version=st.text(), blob=st.binary()) def test_fuzz_reproduce_failure(version: str, blob: bytes) -> None: hypothesis.reproduce_failure(version=version, blob=blob) @given(seed=st.from_type(Hashable)) def test_fuzz_seed(seed: collections.abc.Hashable) -> None: hypothesis.seed(seed=seed) @given( parent=st.none(), max_examples=st.just(not_set), derandomize=st.just(not_set), database=st.just(not_set), verbosity=st.just(not_set), phases=st.just(not_set), stateful_step_count=st.just(not_set), report_multiple_bugs=st.just(not_set), suppress_health_check=st.just(not_set), deadline=st.just(not_set), print_blob=st.just(not_set), backend=st.just(not_set), ) def test_fuzz_settings( parent: typing.Optional[hypothesis.settings], max_examples: int, derandomize: bool, database, verbosity: hypothesis.Verbosity, phases, stateful_step_count: int, report_multiple_bugs: bool, suppress_health_check, deadline: typing.Union[int, float, datetime.timedelta, None], print_blob: bool, backend: str, ) -> None: hypothesis.settings( parent=parent, max_examples=max_examples, derandomize=derandomize, database=database, verbosity=verbosity, phases=phases, stateful_step_count=stateful_step_count, report_multiple_bugs=report_multiple_bugs, suppress_health_check=suppress_health_check, deadline=deadline, print_blob=print_blob, backend=backend, ) @given(name=st.text()) def test_fuzz_settings_get_profile(name: str) -> None: hypothesis.settings.get_profile(name=name) @given(name=st.text()) def test_fuzz_settings_load_profile(name: str) -> None: hypothesis.settings.load_profile(name=name) @given(name=st.text(), parent=st.from_type(typing.Optional[hypothesis.settings])) def test_fuzz_settings_register_profile( name: str, parent: typing.Optional[hypothesis.settings] ) -> None: hypothesis.settings.register_profile(name=name, parent=parent) @given(observation=st.one_of(st.floats(), st.integers()), label=st.text()) def test_fuzz_target(observation: typing.Union[int, float], label: str) -> None: hypothesis.target(observation=observation, label=label) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/invalid_types.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import example_code.future_annotations from hypothesis import given, strategies as st # TODO: replace st.nothing() with appropriate strategies @given(attr1=st.nothing(), attr2=st.nothing(), attr3=st.nothing()) def test_fuzz_invalid_types(attr1, attr2, attr3) -> None: example_code.future_annotations.invalid_types(attr1=attr1, attr2=attr2, attr3=attr3) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/magic_base64_roundtrip.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import base64 from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy @given(altchars=st.none(), s=st.nothing(), validate=st.booleans()) def test_roundtrip_b64encode_b64decode(altchars, s, validate): value0 = base64.b64encode(s=s, altchars=altchars) value1 = base64.b64decode(s=value0, altchars=altchars, validate=validate) assert s == value1, (s, value1) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/magic_base64_roundtrip_with_annotations.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import base64 from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy @given(altchars=st.none(), s=st.nothing(), validate=st.booleans()) def test_roundtrip_b64encode_b64decode(altchars, s, validate) -> None: value0 = base64.b64encode(s=s, altchars=altchars) value1 = base64.b64decode(s=value0, altchars=altchars, validate=validate) assert s == value1, (s, value1) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/magic_builtins.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. from hypothesis import given, strategies as st # TODO: replace st.nothing() with appropriate strategies @given(x=st.nothing()) def test_fuzz_abs(x): abs(x) @given(async_iterable=st.nothing()) def test_fuzz_aiter(async_iterable): aiter(async_iterable) @given(iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text()))) def test_fuzz_all(iterable): all(iterable) @given(iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text()))) def test_fuzz_any(iterable): any(iterable) @given(obj=st.nothing()) def test_fuzz_ascii(obj): ascii(obj) @given(number=st.one_of(st.integers(), st.floats())) def test_fuzz_bin(number): bin(number) @given(frm=st.nothing(), to=st.nothing()) def test_fuzz_bytearray_maketrans(frm, to): bytearray.maketrans(frm, to) @given(frm=st.nothing(), to=st.nothing()) def test_fuzz_bytes_maketrans(frm, to): bytes.maketrans(frm, to) @given(obj=st.nothing()) def test_fuzz_callable(obj): callable(obj) @given(i=st.nothing()) def test_fuzz_chr(i): chr(i) @given( source=st.nothing(), filename=st.nothing(), mode=st.nothing(), flags=st.just(0), dont_inherit=st.booleans(), optimize=st.just(-1), _feature_version=st.just(-1), ) def test_fuzz_compile( source, filename, mode, flags, dont_inherit, optimize, _feature_version ): compile( source=source, filename=filename, mode=mode, flags=flags, dont_inherit=dont_inherit, optimize=optimize, _feature_version=_feature_version, ) @given(real=st.just(0), imag=st.just(0)) def test_fuzz_complex(real, imag): complex(real=real, imag=imag) @given(obj=st.nothing(), name=st.text()) def test_fuzz_delattr(obj, name): delattr(obj, name) @given(object=st.builds(object)) def test_fuzz_dir(object): dir(object) @given(x=st.nothing(), y=st.nothing()) def test_fuzz_divmod(x, y): divmod(x, y) @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), start=st.just(0), ) def test_fuzz_enumerate(iterable, start): enumerate(iterable=iterable, start=start) @given(source=st.nothing(), globals=st.none(), locals=st.none()) def test_fuzz_eval(source, globals, locals): eval(source, globals, locals) @given(source=st.nothing(), globals=st.none(), locals=st.none()) def test_fuzz_exec(source, globals, locals): exec(source, globals, locals) @given(x=st.just(0)) def test_fuzz_float(x): float(x) @given(value=st.nothing(), format_spec=st.just("")) def test_fuzz_format(value, format_spec): format(value, format_spec) @given(object=st.builds(object), name=st.text(), default=st.nothing()) def test_fuzz_getattr(object, name, default): getattr(object, name, default) @given(obj=st.nothing(), name=st.text()) def test_fuzz_hasattr(obj, name): hasattr(obj, name) @given(obj=st.nothing()) def test_fuzz_hash(obj): hash(obj) @given(number=st.one_of(st.integers(), st.floats())) def test_fuzz_hex(number): hex(number) @given(obj=st.nothing()) def test_fuzz_id(obj): id(obj) @given(prompt=st.just("")) def test_fuzz_input(prompt): input(prompt) @given(obj=st.nothing(), class_or_tuple=st.nothing()) def test_fuzz_isinstance(obj, class_or_tuple): isinstance(obj, class_or_tuple) @given(cls=st.nothing(), class_or_tuple=st.nothing()) def test_fuzz_issubclass(cls, class_or_tuple): issubclass(cls, class_or_tuple) @given(iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text()))) def test_fuzz_iter(iterable): iter(iterable) @given(obj=st.nothing()) def test_fuzz_len(obj): len(obj) @given(iterable=st.just(())) def test_fuzz_list(iterable): list(iterable) @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), default=st.nothing(), key=st.nothing(), ) def test_fuzz_max(iterable, default, key): max(iterable, default=default, key=key) @given(object=st.builds(object)) def test_fuzz_memoryview(object): memoryview(object=object) @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), default=st.nothing(), key=st.nothing(), ) def test_fuzz_min(iterable, default, key): min(iterable, default=default, key=key) @given(iterator=st.nothing(), default=st.nothing()) def test_fuzz_next(iterator, default): next(iterator, default) @given(number=st.one_of(st.integers(), st.floats())) def test_fuzz_oct(number): oct(number) @given( file=st.nothing(), mode=st.just("r"), buffering=st.just(-1), encoding=st.none(), errors=st.none(), newline=st.none(), closefd=st.booleans(), opener=st.none(), ) def test_fuzz_open(file, mode, buffering, encoding, errors, newline, closefd, opener): open( file=file, mode=mode, buffering=buffering, encoding=encoding, errors=errors, newline=newline, closefd=closefd, opener=opener, ) @given(c=st.nothing()) def test_fuzz_ord(c): ord(c) @given(base=st.nothing(), exp=st.nothing(), mod=st.none()) def test_fuzz_pow(base, exp, mod): pow(base=base, exp=exp, mod=mod) @given( value=st.nothing(), sep=st.text(), end=st.nothing(), file=st.nothing(), flush=st.nothing(), ) def test_fuzz_print(value, sep, end, file, flush): print(value, sep=sep, end=end, file=file, flush=flush) @given(fget=st.none(), fset=st.none(), fdel=st.none(), doc=st.none()) def test_fuzz_property(fget, fset, fdel, doc): property(fget=fget, fset=fset, fdel=fdel, doc=doc) @given(obj=st.nothing()) def test_fuzz_repr(obj): repr(obj) @given(sequence=st.nothing()) def test_fuzz_reversed(sequence): reversed(sequence) @given(number=st.one_of(st.integers(), st.floats()), ndigits=st.none()) def test_fuzz_round(number, ndigits): round(number=number, ndigits=ndigits) @given(obj=st.nothing(), name=st.text(), value=st.nothing()) def test_fuzz_setattr(obj, name, value): setattr(obj, name, value) @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_fuzz_sorted(iterable, key, reverse): sorted(iterable, key=key, reverse=reverse) @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), start=st.just(0), ) def test_fuzz_sum(iterable, start): sum(iterable, start=start) @given(iterable=st.just(())) def test_fuzz_tuple(iterable): tuple(iterable) @given(object=st.builds(object)) def test_fuzz_vars(object): vars(object) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/magic_class.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output from hypothesis import given, strategies as st @given(arg=st.integers()) def test_fuzz_A_Class_a_classmethod(arg: int) -> None: test_expected_output.A_Class.a_classmethod(arg=arg) @given(arg=st.integers()) def test_fuzz_A_Class_a_staticmethod(arg: int) -> None: test_expected_output.A_Class.a_staticmethod(arg=arg) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/magic_gufunc.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import numpy from hypothesis import given, strategies as st from hypothesis.extra.numpy import arrays, mutually_broadcastable_shapes @given( data=st.data(), shapes=mutually_broadcastable_shapes(signature="(n?,k),(k,m?)->(n?,m?)"), types=st.sampled_from([sig for sig in numpy.matmul.types if "O" not in sig]), ) def test_gufunc_matmul(data, shapes, types): input_shapes, expected_shape = shapes input_dtypes, expected_dtype = types.split("->") array_strats = [ arrays(dtype=dtp, shape=shp, elements={"allow_nan": True}) for dtp, shp in zip(input_dtypes, input_shapes) ] a, b = data.draw(st.tuples(*array_strats)) result = numpy.matmul(a, b) assert result.shape == expected_shape assert result.dtype.char == expected_dtype ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/magic_numpy.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import numpy import test_expected_output from hypothesis import given, strategies as st from hypothesis.extra.numpy import array_shapes, arrays from numpy import dtype @given( f=arrays(dtype=dtype("float64"), shape=array_shapes(max_dims=2)), fc=arrays(dtype=numpy.float64 | numpy.complex128, shape=array_shapes(max_dims=2)), union=st.one_of( st.none(), arrays(dtype=numpy.float64 | numpy.complex128, shape=array_shapes(max_dims=2)), ), ) def test_fuzz_various_numpy_annotations(f, fc, union): test_expected_output.various_numpy_annotations(f=f, fc=fc, union=union) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/matmul_magic.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy matmul_operands = st.nothing() @given(a=matmul_operands, b=matmul_operands, c=matmul_operands) def test_associative_binary_operation_matmul(a, b, c): left = operator.matmul(a, operator.matmul(b, c)) right = operator.matmul(operator.matmul(a, b), c) assert left == right, (left, right) @given(a=matmul_operands) def test_identity_binary_operation_matmul(a): identity = "identity element here" assert a == operator.matmul(a, identity) assert a == operator.matmul(identity, a) @given(a=matmul_operands, b=matmul_operands, c=matmul_operands) def test_add_distributes_over_binary_operation_matmul(a, b, c): left = operator.matmul(a, operator.add(b, c)) ldist = operator.add(operator.matmul(a, b), operator.matmul(a, c)) assert ldist == left, (ldist, left) right = operator.matmul(operator.add(a, b), c) rdist = operator.add(operator.matmul(a, c), operator.matmul(b, c)) assert rdist == right, (rdist, right) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/merge_dicts.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import collections.abc import example_code.future_annotations from collections import ChainMap from hypothesis import given, strategies as st @given( map1=st.one_of( st.dictionaries(keys=st.text(), values=st.integers()).map(ChainMap), st.dictionaries(keys=st.text(), values=st.integers()), ), map2=st.one_of( st.dictionaries(keys=st.text(), values=st.integers()).map(ChainMap), st.dictionaries(keys=st.text(), values=st.integers()), ), ) def test_fuzz_merge_dicts( map1: collections.abc.Mapping[str, int], map2: collections.abc.Mapping[str, int] ) -> None: example_code.future_annotations.merge_dicts(map1=map1, map2=map2) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/multiplication_magic.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy mul_operands = st.nothing() @given(a=mul_operands, b=mul_operands, c=mul_operands) def test_associative_binary_operation_mul(a, b, c): left = operator.mul(a, operator.mul(b, c)) right = operator.mul(operator.mul(a, b), c) assert left == right, (left, right) @given(a=mul_operands, b=mul_operands) def test_commutative_binary_operation_mul(a, b): left = operator.mul(a, b) right = operator.mul(b, a) assert left == right, (left, right) @given(a=mul_operands) def test_identity_binary_operation_mul(a): identity = "identity element here" assert a == operator.mul(a, identity) assert a == operator.mul(identity, a) @given(a=mul_operands, b=mul_operands, c=mul_operands) def test_add_distributes_over_binary_operation_mul(a, b, c): left = operator.mul(a, operator.add(b, c)) ldist = operator.add(operator.mul(a, b), operator.mul(a, c)) assert ldist == left, (ldist, left) right = operator.mul(operator.add(a, b), c) rdist = operator.add(operator.mul(a, c), operator.mul(b, c)) assert rdist == right, (rdist, right) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/multiplication_operator.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy mul_operands = st.nothing() @given(a=mul_operands, b=mul_operands, c=mul_operands) def test_associative_binary_operation_mul(a, b, c): left = operator.mul(a, operator.mul(b, c)) right = operator.mul(operator.mul(a, b), c) assert left == right, (left, right) @given(a=mul_operands, b=mul_operands) def test_commutative_binary_operation_mul(a, b): left = operator.mul(a, b) right = operator.mul(b, a) assert left == right, (left, right) @given(a=mul_operands) def test_identity_binary_operation_mul(a): identity = 1 assert a == operator.mul(a, identity) assert a == operator.mul(identity, a) @given(a=mul_operands, b=mul_operands, c=mul_operands) def test_add_distributes_over_binary_operation_mul(a, b, c): left = operator.mul(a, operator.add(b, c)) ldist = operator.add(operator.mul(a, b), operator.mul(a, c)) assert ldist == left, (ldist, left) right = operator.mul(operator.add(a, b), c) rdist = operator.add(operator.mul(a, c), operator.mul(b, c)) assert rdist == right, (rdist, right) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/multiplication_operator_unittest.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import operator import unittest from hypothesis import given, strategies as st # TODO: replace st.nothing() with an appropriate strategy class TestBinaryOperationmul(unittest.TestCase): mul_operands = st.nothing() @given(a=mul_operands, b=mul_operands, c=mul_operands) def test_associative_binary_operation_mul(self, a, b, c): left = operator.mul(a, operator.mul(b, c)) right = operator.mul(operator.mul(a, b), c) self.assertEqual(left, right) @given(a=mul_operands, b=mul_operands) def test_commutative_binary_operation_mul(self, a, b): left = operator.mul(a, b) right = operator.mul(b, a) self.assertEqual(left, right) @given(a=mul_operands) def test_identity_binary_operation_mul(self, a): identity = 1 self.assertEqual(a, operator.mul(a, identity)) self.assertEqual(a, operator.mul(identity, a)) @given(a=mul_operands, b=mul_operands, c=mul_operands) def test_add_distributes_over_binary_operation_mul(self, a, b, c): left = operator.mul(a, operator.add(b, c)) ldist = operator.add(operator.mul(a, b), operator.mul(a, c)) self.assertEqual(ldist, left) right = operator.mul(operator.add(a, b), c) rdist = operator.add(operator.mul(a, c), operator.mul(b, c)) self.assertEqual(rdist, right) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/nothing_found.txt ================================================ # Found no testable functions in foo (from 'foo/__init__.py') # Try writing tests for submodules, e.g. by using: # hypothesis write foo.bar ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/optional_parameter.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output import typing from hypothesis import given, strategies as st @given(a=st.floats(), b=st.one_of(st.none(), st.floats())) def test_fuzz_optional_parameter(a: float, b: typing.Union[float, None]) -> None: test_expected_output.optional_parameter(a=a, b=b) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/optional_union_parameter.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import test_expected_output import typing from hypothesis import given, strategies as st @given(a=st.floats(), b=st.one_of(st.none(), st.floats(), st.integers())) def test_fuzz_optional_union_parameter( a: float, b: typing.Union[float, int, None] ) -> None: test_expected_output.optional_union_parameter(a=a, b=b) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/re_compile.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import re from hypothesis import given, strategies as st @given(pattern=st.text(), flags=st.just(0)) def test_fuzz_compile(pattern, flags): re.compile(pattern=pattern, flags=flags) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/re_compile_except.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import re from hypothesis import given, reject, strategies as st @given(pattern=st.text(), flags=st.just(0)) def test_fuzz_compile(pattern, flags): try: re.compile(pattern=pattern, flags=flags) except re.error: reject() ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/re_compile_unittest.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import re import unittest from hypothesis import given, strategies as st class TestFuzzCompile(unittest.TestCase): @given(pattern=st.text(), flags=st.just(0)) def test_fuzz_compile(self, pattern, flags): re.compile(pattern=pattern, flags=flags) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/sequence_from_collections.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import collections.abc import test_expected_output from hypothesis import given, strategies as st @given(items=st.one_of(st.binary(), st.lists(st.integers()))) def test_fuzz_sequence_from_collections(items: collections.abc.Sequence[int]) -> None: test_expected_output.sequence_from_collections(items=items) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/sorted_idempotent.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. from hypothesis import given, strategies as st @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_idempotent_sorted(iterable, key, reverse): result = sorted(iterable, key=key, reverse=reverse) repeat = sorted(result, key=key, reverse=reverse) assert result == repeat, (result, repeat) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/sorted_self_equivalent.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. from hypothesis import given, strategies as st @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_equivalent_sorted_sorted_sorted(iterable, key, reverse): result_0_sorted = sorted(iterable, key=key, reverse=reverse) result_1_sorted = sorted(iterable, key=key, reverse=reverse) result_2_sorted = sorted(iterable, key=key, reverse=reverse) assert result_0_sorted == result_1_sorted, (result_0_sorted, result_1_sorted) assert result_0_sorted == result_2_sorted, (result_0_sorted, result_2_sorted) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/sorted_self_equivalent_with_annotations.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. from hypothesis import given, strategies as st @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_equivalent_sorted_sorted_sorted(iterable, key, reverse) -> None: result_0_sorted = sorted(iterable, key=key, reverse=reverse) result_1_sorted = sorted(iterable, key=key, reverse=reverse) result_2_sorted = sorted(iterable, key=key, reverse=reverse) assert result_0_sorted == result_1_sorted, (result_0_sorted, result_1_sorted) assert result_0_sorted == result_2_sorted, (result_0_sorted, result_2_sorted) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/sorted_self_error_equivalent_1error.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import pytest from hypothesis import given, reject, strategies as st, target @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_equivalent_sorted_sorted(iterable, key, reverse): try: result_0_sorted = sorted(iterable, key=key, reverse=reverse) exc_type = None target(1, label="input was valid") except ValueError: reject() except Exception as exc: exc_type = type(exc) if exc_type: with pytest.raises(exc_type): sorted(iterable, key=key, reverse=reverse) else: result_1_sorted = sorted(iterable, key=key, reverse=reverse) assert result_0_sorted == result_1_sorted, (result_0_sorted, result_1_sorted) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/sorted_self_error_equivalent_2error_unittest.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import unittest from hypothesis import given, reject, strategies as st, target class TestEquivalentSortedSorted(unittest.TestCase): @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_equivalent_sorted_sorted(self, iterable, key, reverse): try: result_0_sorted = sorted(iterable, key=key, reverse=reverse) exc_type = None target(1, label="input was valid") except (TypeError, ValueError): reject() except Exception as exc: exc_type = type(exc) if exc_type: with self.assertRaises(exc_type): sorted(iterable, key=key, reverse=reverse) else: result_1_sorted = sorted(iterable, key=key, reverse=reverse) self.assertEqual(result_0_sorted, result_1_sorted) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/sorted_self_error_equivalent_simple.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import pytest from hypothesis import given, strategies as st, target @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_equivalent_sorted_sorted(iterable, key, reverse): try: result_0_sorted = sorted(iterable, key=key, reverse=reverse) exc_type = None target(1, label="input was valid") except Exception as exc: exc_type = type(exc) if exc_type: with pytest.raises(exc_type): sorted(iterable, key=key, reverse=reverse) else: result_1_sorted = sorted(iterable, key=key, reverse=reverse) assert result_0_sorted == result_1_sorted, (result_0_sorted, result_1_sorted) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/sorted_self_error_equivalent_threefuncs.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import pytest from hypothesis import given, strategies as st, target @given( iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())), key=st.none(), reverse=st.booleans(), ) def test_equivalent_sorted_sorted_sorted(iterable, key, reverse): try: result_0_sorted = sorted(iterable, key=key, reverse=reverse) exc_type = None target(1, label="input was valid") except Exception as exc: exc_type = type(exc) if exc_type: with pytest.raises(exc_type): sorted(iterable, key=key, reverse=reverse) else: result_1_sorted = sorted(iterable, key=key, reverse=reverse) assert result_0_sorted == result_1_sorted, (result_0_sorted, result_1_sorted) if exc_type: with pytest.raises(exc_type): sorted(iterable, key=key, reverse=reverse) else: result_2_sorted = sorted(iterable, key=key, reverse=reverse) assert result_0_sorted == result_2_sorted, (result_0_sorted, result_2_sorted) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import collections.abc import test_expected_output from hypothesis import given, strategies as st @given(seq=st.one_of(st.binary(), st.lists(st.integers()))) def test_idempotent_timsort(seq: collections.abc.Sequence[int]) -> None: result = test_expected_output.timsort(seq=seq) repeat = test_expected_output.timsort(seq=result) assert result == repeat, (result, repeat) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent_asserts.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import collections.abc import test_expected_output from hypothesis import given, reject, strategies as st @given(seq=st.one_of(st.binary(), st.lists(st.integers()))) def test_idempotent_timsort(seq: collections.abc.Sequence[int]) -> None: try: result = test_expected_output.timsort(seq=seq) repeat = test_expected_output.timsort(seq=result) except AssertionError: reject() assert result == repeat, (result, repeat) ================================================ FILE: hypothesis-python/tests/ghostwriter/recorded/union_sequence_parameter.txt ================================================ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. import collections.abc import test_expected_output import typing from hypothesis import given, strategies as st @given(items=st.one_of(st.binary(), st.lists(st.one_of(st.floats(), st.integers())))) def test_fuzz_union_sequence_parameter( items: collections.abc.Sequence[typing.Union[float, int]], ) -> None: test_expected_output.union_sequence_parameter(items=items) ================================================ FILE: hypothesis-python/tests/ghostwriter/test_expected_output.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ 'Golden master' tests for the ghostwriter. To update the recorded outputs, run: pytest test_expected_output.py --hypothesis-update-outputs """ import ast import base64 import builtins import collections.abc import operator import pathlib import re import subprocess import sys from collections.abc import Sequence import black import numpy import numpy.typing import pytest from example_code.future_annotations import ( add_custom_classes, invalid_types, merge_dicts, ) import hypothesis from hypothesis import settings from hypothesis.extra import ghostwriter from hypothesis.utils.conventions import not_set pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason="ghostwriter is not thread safe", ) @pytest.fixture def update_recorded_outputs(request): return request.config.getoption("--hypothesis-update-outputs") def get_recorded(name, actual=""): file_ = pathlib.Path(__file__).parent / "recorded" / f"{name}.txt" if actual: file_.write_text(actual, encoding="utf-8") return file_.read_text(encoding="utf-8") def timsort(seq: Sequence[int]) -> Sequence[int]: return sorted(seq) def with_docstring(a, b, c, d=int, e=lambda x: f"xx{x}xx") -> None: """Demonstrates parsing params from the docstring :param a: sphinx docstring style :type a: sequence of integers b (list, tuple, or None): Google docstring style c : {"foo", "bar", or None} Numpy docstring style """ class A_Class: @classmethod def a_classmethod(cls, arg: int): pass @staticmethod def a_staticmethod(arg: int): pass def add(a: float, b: float) -> float: return a + b def divide(a: int, b: int) -> float: """This is a RST-style docstring for `divide`. :raises ZeroDivisionError: if b == 0 """ return a / b def optional_parameter(a: float, b: float | None) -> float: return optional_union_parameter(a, b) def optional_union_parameter(a: float, b: float | int | None) -> float: return a if b is None else a + b def union_sequence_parameter(items: Sequence[float | int]) -> float: return sum(items) def sequence_from_collections(items: collections.abc.Sequence[int]) -> int: return min(items) def various_numpy_annotations( f: numpy.typing.NDArray[numpy.float64], fc: numpy.typing.NDArray[numpy.float64 | numpy.complex128], union: numpy.typing.NDArray[numpy.float64 | numpy.complex128] | None, ): pass # Note: for some of the `expected` outputs, we replace away some small # parts which vary between minor versions of Python. @pytest.mark.parametrize( "data", [ ("fuzz_sorted", lambda: ghostwriter.fuzz(sorted)), ( "fuzz_sorted_with_annotations", lambda: ghostwriter.fuzz(sorted, annotate=True), ), ("fuzz_with_docstring", lambda: ghostwriter.fuzz(with_docstring)), ("fuzz_classmethod", lambda: ghostwriter.fuzz(A_Class.a_classmethod)), ("fuzz_staticmethod", lambda: ghostwriter.fuzz(A_Class.a_staticmethod)), ("fuzz_ufunc", lambda: ghostwriter.fuzz(numpy.add)), ("magic_gufunc", lambda: ghostwriter.magic(numpy.matmul)), ("optional_parameter", lambda: ghostwriter.magic(optional_parameter)), ( "optional_union_parameter", lambda: ghostwriter.magic(optional_union_parameter), ), ( "union_sequence_parameter", lambda: ghostwriter.magic(union_sequence_parameter), ), ( "sequence_from_collections", lambda: ghostwriter.magic(sequence_from_collections), ), ("add_custom_classes", lambda: ghostwriter.magic(add_custom_classes)), ("merge_dicts", lambda: ghostwriter.magic(merge_dicts)), ("invalid_types", lambda: ghostwriter.magic(invalid_types)), ("magic_base64_roundtrip", lambda: ghostwriter.magic(base64.b64encode)), ( "magic_base64_roundtrip_with_annotations", lambda: ghostwriter.magic(base64.b64encode, annotate=True), ), ("re_compile", lambda: ghostwriter.fuzz(re.compile)), ( "re_compile_except", lambda: ghostwriter.fuzz(re.compile, except_=re.error).replace( "re.PatternError", "re.error" # changed in Python 3.13 ), ), ("re_compile_unittest", lambda: ghostwriter.fuzz(re.compile, style="unittest")), pytest.param( ("base64_magic", lambda: ghostwriter.magic(base64)), marks=pytest.mark.skipif("sys.version_info[:2] >= (3, 10)"), ), ("sorted_idempotent", lambda: ghostwriter.idempotent(sorted)), ("timsort_idempotent", lambda: ghostwriter.idempotent(timsort)), ( "timsort_idempotent_asserts", lambda: ghostwriter.idempotent(timsort, except_=AssertionError), ), pytest.param( ("eval_equivalent", lambda: ghostwriter.equivalent(eval, ast.literal_eval)), marks=[pytest.mark.skipif(sys.version_info[:2] >= (3, 13), reason="kw")], ), ( "sorted_self_equivalent", lambda: ghostwriter.equivalent(sorted, sorted, sorted), ), ( "sorted_self_equivalent_with_annotations", lambda: ghostwriter.equivalent(sorted, sorted, sorted, annotate=True), ), ("addition_op_magic", lambda: ghostwriter.magic(add)), ("multiplication_magic", lambda: ghostwriter.magic(operator.mul)), ("matmul_magic", lambda: ghostwriter.magic(operator.matmul)), ( "addition_op_multimagic", lambda: ghostwriter.magic(add, operator.add, numpy.add), ), ("division_fuzz_error_handler", lambda: ghostwriter.fuzz(divide)), ( "division_binop_error_handler", lambda: ghostwriter.binary_operation(divide, identity=1), ), ( "division_roundtrip_error_handler", lambda: ghostwriter.roundtrip(divide, operator.mul), ), ( "division_roundtrip_error_handler_without_annotations", lambda: ghostwriter.roundtrip(divide, operator.mul, annotate=False), ), ( "division_roundtrip_arithmeticerror_handler", lambda: ghostwriter.roundtrip( divide, operator.mul, except_=ArithmeticError ), ), ( "division_roundtrip_typeerror_handler", lambda: ghostwriter.roundtrip(divide, operator.mul, except_=TypeError), ), ( "division_operator", lambda: ghostwriter.binary_operation( operator.truediv, associative=False, commutative=False ), ), ( "division_operator_with_annotations", lambda: ghostwriter.binary_operation( operator.truediv, associative=False, commutative=False, annotate=True ), ), ( "multiplication_operator", lambda: ghostwriter.binary_operation( operator.mul, identity=1, distributes_over=operator.add ), ), ( "multiplication_operator_unittest", lambda: ghostwriter.binary_operation( operator.mul, identity=1, distributes_over=operator.add, style="unittest", ), ), ( "sorted_self_error_equivalent_simple", lambda: ghostwriter.equivalent(sorted, sorted, allow_same_errors=True), ), ( "sorted_self_error_equivalent_threefuncs", lambda: ghostwriter.equivalent( sorted, sorted, sorted, allow_same_errors=True ), ), ( "sorted_self_error_equivalent_1error", lambda: ghostwriter.equivalent( sorted, sorted, allow_same_errors=True, except_=ValueError, ), ), ( "sorted_self_error_equivalent_2error_unittest", lambda: ghostwriter.equivalent( sorted, sorted, allow_same_errors=True, except_=(TypeError, ValueError), style="unittest", ), ), ("magic_class", lambda: ghostwriter.magic(A_Class)), pytest.param( ("magic_builtins", lambda: ghostwriter.magic(builtins)), marks=[ pytest.mark.skipif( sys.version_info[:2] != (3, 10), reason="often small changes", ) ], ), pytest.param( ( "magic_numpy", lambda: ghostwriter.magic(various_numpy_annotations, annotate=False), ), marks=pytest.mark.skipif(various_numpy_annotations is add, reason="<=3.9"), ), ], ids=lambda x: x[0], ) def test_ghostwriter_example_outputs(update_recorded_outputs, data): name, get_actual = data # ghostwriter computations can be expensive, so defer collection-time # computations until test-time actual = get_actual() expected = get_recorded(name, actual * update_recorded_outputs) assert actual == expected # We got the expected source code exec(expected, {}) # and there are no SyntaxError or NameErrors def test_ghostwriter_on_hypothesis(update_recorded_outputs): actual = ( ghostwriter.magic(hypothesis) .replace("Strategy[+Ex]", "Strategy") .replace("hypothesis._settings.settings", "hypothesis.settings") ) # hypothesis._settings.settings wraps the line before replacement, and doesn't # after replacement actual = black.format_str(actual, mode=black.Mode()) expected = get_recorded("hypothesis_module_magic", actual * update_recorded_outputs) if sys.version_info[:2] == (3, 10): assert actual == expected exec(expected, {"not_set": not_set}) def test_ghostwriter_suggests_submodules_for_empty_toplevel( tmp_path, update_recorded_outputs ): foo = tmp_path / "foo" foo.mkdir() (foo / "__init__.py").write_text("from . import bar\n", encoding="utf-8") (foo / "bar.py").write_text("def baz(x: int): ...\n", encoding="utf-8") proc = subprocess.run( ["hypothesis", "write", "foo"], check=True, capture_output=True, encoding="utf-8", cwd=tmp_path, ) actual = proc.stdout.replace(re.search(r"from '(.+)foo/", proc.stdout).group(1), "") expected = get_recorded("nothing_found", actual * update_recorded_outputs) assert actual == expected # We got the expected source code exec(expected, {}) # and there are no SyntaxError or NameErrors ================================================ FILE: hypothesis-python/tests/ghostwriter/test_ghostwriter.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import ast import enum import json import re import socket import sys import unittest import unittest.mock from collections.abc import KeysView, Sequence, Sized, ValuesView from decimal import Decimal from pathlib import Path from textwrap import dedent from types import FunctionType, ModuleType from typing import Any, ForwardRef import attr import click import pytest from hypothesis import HealthCheck, assume, settings from hypothesis.errors import InvalidArgument, Unsatisfiable from hypothesis.extra import cli, ghostwriter from hypothesis.internal.compat import BaseExceptionGroup from hypothesis.strategies import builds, from_type, just, lists from hypothesis.strategies._internal.core import from_regex from hypothesis.strategies._internal.lazy import LazyStrategy varied_excepts = pytest.mark.parametrize("ex", [(), ValueError, (TypeError, re.error)]) pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason="ghostwriter is not thread safe", ) def get_test_function(source_code, settings_decorator=lambda fn: fn): # A helper function to get the dynamically-defined test function. # Note that this also tests that the module is syntatically-valid, # AND free from undefined names, import problems, and so on. namespace = {} try: exec(source_code, namespace) except Exception: print(f"************\n{source_code}\n************") raise tests = [ v for k, v in namespace.items() if k.startswith(("test_", "Test")) and not isinstance(v, ModuleType) ] assert len(tests) == 1, tests return settings_decorator(tests[0]) @pytest.mark.parametrize( "badness", ["not an exception", BaseException, [ValueError], (Exception, "bad")] ) def test_invalid_exceptions(badness): with pytest.raises(InvalidArgument): ghostwriter._check_except(badness) def test_style_validation(): ghostwriter._check_style("pytest") ghostwriter._check_style("unittest") with pytest.raises(InvalidArgument): ghostwriter._check_style("not a valid style") def test_strategies_with_invalid_syntax_repr_as_nothing(): msg = "$$ this repr is not Python syntax $$" class NoRepr: def __repr__(self): return msg s = just(NoRepr()) assert repr(s) == f"just({msg})" assert ghostwriter._valid_syntax_repr(s)[1] == "nothing()" class AnEnum(enum.Enum): a = "value of AnEnum.a" b = "value of AnEnum.b" def takes_enum(foo=AnEnum.a): # This can only fail if we use the default argument to guess # that any instance of that enum type should be allowed. assert foo != AnEnum.b def test_ghostwriter_exploits_arguments_with_enum_defaults(): source_code = ghostwriter.fuzz(takes_enum) test = get_test_function(source_code) with pytest.raises(AssertionError): test() def timsort(seq: Sequence[int]) -> list[int]: return sorted(seq) def non_type_annotation(x: 3): # type: ignore pass def annotated_any(x: Any): pass space_in_name = type("a name", (type,), {"__init__": lambda self: None}) class NotResolvable: def __init__(self, unannotated_required): pass def non_resolvable_arg(x: NotResolvable): pass def test_flattens_one_of_repr(): strat = from_type(int | Sequence[int]) assert repr(strat).count("one_of(") == 2 assert ghostwriter._valid_syntax_repr(strat)[1].count("one_of(") == 1 def takes_keys(x: KeysView[int]) -> None: pass def takes_values(x: ValuesView[int]) -> None: pass def takes_match(x: re.Match[bytes]) -> None: pass def takes_pattern(x: re.Pattern[str]) -> None: pass def takes_sized(x: Sized) -> None: pass def takes_frozensets(a: frozenset[int], b: frozenset[int]) -> None: pass @attr.s() class Foo: foo: str = attr.ib() def takes_attrs_class(x: Foo) -> None: pass @varied_excepts @pytest.mark.parametrize( "func", [ re.compile, json.loads, json.dump, timsort, ast.literal_eval, non_type_annotation, annotated_any, space_in_name, non_resolvable_arg, takes_keys, takes_values, takes_match, takes_pattern, takes_sized, takes_frozensets, takes_attrs_class, ], ) def test_ghostwriter_fuzz(func, ex): source_code = ghostwriter.fuzz(func, except_=ex) get_test_function(source_code) def test_socket_module(): source_code = ghostwriter.magic(socket) exec(source_code, {}) def test_binary_op_also_handles_frozensets(): # Using str.replace in a loop would convert `frozensets()` into # `st.frozenst.sets()` instead of `st.frozensets()`; fixed with re.sub. source_code = ghostwriter.binary_operation(takes_frozensets) exec(source_code, {}) def test_binary_op_with_numpy_arrays_includes_imports(): # Regression test for issue #4576: binary_operation should include imports # for numpy strategies like arrays(), scalar_dtypes(), and array_shapes() pytest.importorskip("numpy") import numpy as np def numpy_add(a: np.ndarray, b: np.ndarray) -> np.ndarray: return a + b source_code = ghostwriter.binary_operation( numpy_add, associative=True, commutative=True, identity=None ) # Check that the necessary imports are present assert "from hypothesis.extra.numpy import" in source_code assert "arrays" in source_code assert "scalar_dtypes" in source_code assert "array_shapes" in source_code # Most importantly: the code should execute without NameError exec(source_code, {}) @varied_excepts @pytest.mark.parametrize( "func", [re.compile, json.loads, json.dump, timsort, ast.literal_eval] ) def test_ghostwriter_unittest_style(func, ex): source_code = ghostwriter.fuzz(func, except_=ex, style="unittest") assert issubclass(get_test_function(source_code), unittest.TestCase) def no_annotations(foo=None, *, bar=False): pass def test_inference_from_defaults_and_none_booleans_reprs_not_just_and_sampled_from(): source_code = ghostwriter.fuzz(no_annotations) assert "@given(foo=st.none(), bar=st.booleans())" in source_code def hopefully_hashable(foo: set[Decimal]): pass def test_no_hashability_filter(): # In from_type, we ordinarily protect users from really weird cases like # `Decimal('snan')` - a unhashable value of a hashable type - but in the # ghostwriter we instead want to present this to the user for an explicit # decision. They can pass `allow_nan=False`, fix their custom type's # hashing logic, or whatever else; simply doing nothing will usually work. source_code = ghostwriter.fuzz(hopefully_hashable) assert "@given(foo=st.sets(st.decimals()))" in source_code assert "_can_hash" not in source_code @pytest.mark.parametrize( "gw,args", [ (ghostwriter.fuzz, ["not callable"]), (ghostwriter.idempotent, ["not callable"]), (ghostwriter.roundtrip, []), (ghostwriter.roundtrip, ["not callable"]), (ghostwriter.equivalent, [sorted]), (ghostwriter.equivalent, [sorted, "not callable"]), ], ) def test_invalid_func_inputs(gw, args): with pytest.raises(InvalidArgument): gw(*args) class A: @classmethod def to_json(cls, obj: dict | list) -> str: return json.dumps(obj) @classmethod def from_json(cls, obj: str) -> dict | list: return json.loads(obj) @staticmethod def static_sorter(seq: Sequence[int]) -> list[int]: return sorted(seq) @pytest.mark.parametrize( "gw,args", [ (ghostwriter.fuzz, [A.static_sorter]), (ghostwriter.idempotent, [A.static_sorter]), (ghostwriter.roundtrip, [A.to_json, A.from_json]), (ghostwriter.equivalent, [A.to_json, json.dumps]), ], ) def test_class_methods_inputs(gw, args): source_code = gw(*args) get_test_function(source_code)() def test_run_ghostwriter_fuzz(): # Our strategy-guessing code works for all the arguments to sorted, # and we handle positional-only arguments in calls correctly too. source_code = ghostwriter.fuzz(sorted) assert "st.nothing()" not in source_code get_test_function(source_code)() class MyError(UnicodeDecodeError): pass @pytest.mark.parametrize( "exceptions,output", [ # Discard subclasses of other exceptions to catch, including non-builtins, # and replace OSError aliases with OSError. ((Exception, UnicodeError), "Exception"), ((UnicodeError, MyError), "UnicodeError"), ((IOError,), "OSError"), ((IOError, UnicodeError), "(OSError, UnicodeError)"), ], ) def test_exception_deduplication(exceptions, output): _, body = ghostwriter._make_test_body( lambda: None, ghost="", test_body="pass", except_=exceptions, style="pytest", annotate=False, ) assert f"except {output}:" in body def test_run_ghostwriter_roundtrip(): # This test covers the whole lifecycle: first, we get the default code. # The first argument is unknown, so we fail to draw from st.nothing() source_code = ghostwriter.roundtrip(json.dumps, json.loads) with pytest.raises(Unsatisfiable): get_test_function(source_code)() # Replacing that nothing() with a strategy for JSON allows us to discover # two possible failures: `nan` is not equal to itself, and if dumps is # passed allow_nan=False it is a ValueError to pass a non-finite float. source_code = source_code.replace( "st.nothing()", "st.recursive(st.one_of(st.none(), st.booleans(), st.floats(), st.text()), " "lambda v: st.lists(v, max_size=2) | st.dictionaries(st.text(), v, max_size=2)" ", max_leaves=2)", ) s = settings(deadline=None, suppress_health_check=[HealthCheck.too_slow]) try: get_test_function(source_code, settings_decorator=s)() except (AssertionError, ValueError, BaseExceptionGroup): pass # Finally, restricting ourselves to finite floats makes the test pass! source_code = source_code.replace( "st.floats()", "st.floats(allow_nan=False, allow_infinity=False)" ) get_test_function(source_code, settings_decorator=s)() @varied_excepts @pytest.mark.parametrize("func", [sorted, timsort]) def test_ghostwriter_idempotent(func, ex): source_code = ghostwriter.idempotent(func, except_=ex) test = get_test_function(source_code) if "=st.nothing()" in source_code: with pytest.raises(Unsatisfiable): test() else: test() def test_overlapping_args_use_union_of_strategies(): def f(arg: int) -> None: pass def g(arg: float) -> None: pass source_code = ghostwriter.equivalent(f, g) assert "arg=st.one_of(st.integers(), st.floats())" in source_code def test_module_with_mock_does_not_break(): # Before we added an explicit check for unspec'd mocks, they would pass # through the initial validation and then fail when used in more detailed # logic in the ghostwriter machinery. ghostwriter.magic(unittest.mock) def compose_types(x: type, y: type): pass def test_unrepr_identity_elem(): # Works with inferred identity element source_code = ghostwriter.binary_operation(compose_types) exec(source_code, {}) # and also works with explicit identity element source_code = ghostwriter.binary_operation(compose_types, identity=type) exec(source_code, {}) @pytest.mark.parametrize( "strategy, imports", # The specifics don't matter much here; we're just demonstrating that # we can walk the strategy and collect all the objects to import. [ # Lazy from_type() is handled without being unwrapped (LazyStrategy(from_type, (enum.Enum,), {}), {("enum", "Enum")}), # Mapped, filtered, and flatmapped check both sides of the method ( builds(enum.Enum).map(Decimal), {("enum", "Enum"), ("decimal", "Decimal")}, ), ( builds(enum.Enum).flatmap(Decimal), {("enum", "Enum"), ("decimal", "Decimal")}, ), ( builds(enum.Enum).filter(Decimal).filter(re.compile), {("enum", "Enum"), ("decimal", "Decimal"), ("re", "compile")}, ), # one_of() strategies recurse into all the branches ( builds(enum.Enum) | builds(Decimal) | builds(re.compile), {("enum", "Enum"), ("decimal", "Decimal"), ("re", "compile")}, ), # and builds() checks the arguments as well as the target ( builds(enum.Enum, builds(Decimal), kw=builds(re.compile)), {("enum", "Enum"), ("decimal", "Decimal"), ("re", "compile")}, ), # lists recurse on imports ( lists(builds(Decimal)), {("decimal", "Decimal")}, ), # find the needed import for from_regex if needed ( from_regex(re.compile(".+")), {"re"}, ), # but don't add superfluous imports ( from_regex(".+"), set(), ), ], ) def test_get_imports_for_strategy(strategy, imports): assert ghostwriter._imports_for_strategy(strategy) == imports @pytest.fixture def temp_script_file(): """Fixture to yield a Path to a temporary file in the local directory. File name will end in .py and will include an importable function. """ p = Path("my_temp_script.py") if p.exists(): raise FileExistsError(f"Did not expect {p} to exist during testing") p.write_text( dedent( """ def say_hello(): print("Hello world!") """ ), encoding="utf-8", ) yield p p.unlink() @pytest.fixture def temp_script_file_with_py_function(): """Fixture to yield a Path to a temporary file in the local directory. File name will end in .py and will include an importable function named "py" """ p = Path("my_temp_script_with_py_function.py") if p.exists(): raise FileExistsError(f"Did not expect {p} to exist during testing") p.write_text( dedent( """ def py(): print('A function named "py" has been called') """ ), encoding="utf-8", ) yield p p.unlink() def test_obj_name(temp_script_file, temp_script_file_with_py_function): # Module paths (strings including a "/") should raise a meaningful UsageError with pytest.raises(click.exceptions.UsageError) as e: cli.obj_name("mydirectory/myscript.py") assert e.match( "Remember that the ghostwriter should be passed the name of a module, not a path." ) # Windows paths (strings including a "\") should also raise a meaningful UsageError with pytest.raises(click.exceptions.UsageError) as e: cli.obj_name(R"mydirectory\myscript.py") assert e.match( "Remember that the ghostwriter should be passed the name of a module, not a path." ) # File names of modules (strings ending in ".py") should raise a meaningful UsageError with pytest.raises(click.exceptions.UsageError) as e: cli.obj_name("myscript.py") assert e.match( "Remember that the ghostwriter should be passed the name of a module, not a file." ) # File names of modules (strings ending in ".py") that exist should get a suggestion with pytest.raises(click.exceptions.UsageError) as e: cli.obj_name(str(temp_script_file)) assert e.match( "Remember that the ghostwriter should be passed the name of a module, not a file." f"\n\tTry: hypothesis write {temp_script_file.stem}" ) # File names of modules (strings ending in ".py") that define a py function should succeed assert isinstance( cli.obj_name(str(temp_script_file_with_py_function)), FunctionType ) def test_gets_public_location_not_impl_location(): assert ghostwriter._get_module(assume) == "hypothesis" # not "hypothesis.control" class ForwardRefA: pass @pytest.mark.parametrize( "parameter, type_name", [ (ForwardRef("this_ref_does_not_exist"), None), # ForwardRef.evaluate() logic is new in 3.14 *( [] if sys.version_info[:2] < (3, 14) else [ ( ForwardRef("ForwardRefA", owner=A), ghostwriter._AnnotationData( "test_ghostwriter.ForwardRefA", {"test_ghostwriter"} ), ) ] ), ], ) def test_parameter_to_annotation(parameter, type_name): assert ghostwriter._parameter_to_annotation(parameter) == type_name ================================================ FILE: hypothesis-python/tests/ghostwriter/test_ghostwriter_cli.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import ast import itertools import json import operator import re import subprocess import sys import pytest from hypothesis import settings, strategies as st from hypothesis.errors import StopTest from hypothesis.extra.ghostwriter import ( binary_operation, equivalent, fuzz, idempotent, magic, roundtrip, ) from hypothesis.internal.reflection import get_pretty_function_description pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason="ghostwriter is not thread safe", ) def run(cmd, *, cwd=None): return subprocess.run( cmd, capture_output=True, shell=True, text=True, cwd=cwd, encoding="utf-8" ) @pytest.mark.parametrize( "cli,code", [ # Passing one argument falls back to one-argument tests ("--equivalent re.compile", lambda: fuzz(re.compile)), ("--roundtrip sorted", lambda: idempotent(sorted)), # For multiple arguments, they're equivalent to the function call ( "--equivalent eval ast.literal_eval", lambda: equivalent(eval, ast.literal_eval), ), ( "--roundtrip json.loads json.dumps --except ValueError", lambda: roundtrip(json.loads, json.dumps, except_=ValueError), ), # Imports submodule (importlib.import_module passes; __import__ fails) pytest.param( "hypothesis.strategies", lambda: magic(st), marks=pytest.mark.skipif(sys.version_info[:2] != (3, 10), reason="varies"), ), # We can write tests for classes even without classmethods or staticmethods ("hypothesis.errors.StopTest", lambda: fuzz(StopTest)), # Search for identity element does not print e.g. "You can use @seed ..." ("--binary-op operator.add", lambda: binary_operation(operator.add)), # Annotations are passed correctly ("sorted --annotate", lambda: fuzz(sorted, annotate=True)), ("sorted --no-annotate", lambda: fuzz(sorted, annotate=False)), ], ids=get_pretty_function_description, ) def test_cli_python_equivalence(cli, code): result = run("hypothesis write " + cli) result.check_returncode() cli_output = result.stdout.strip() assert cli == "hypothesis.strategies" or not result.stderr code_output = code().strip() assert code_output == cli_output @pytest.mark.parametrize( "cli,err_msg", [ ("--idempotent sorted sorted", "Test functions for idempotence one at a time."), ( "xxxx", "Found the 'builtins' module, but it doesn't have a 'xxxx' attribute.", ), ( "re.srch", "Found the 're' module, but it doesn't have a 'srch' attribute. " "Closest matches: ['search']", ), ( "re.fmatch", "Found the 're' module, but it doesn't have a 'fmatch' attribute. " "Closest matches: ['match', 'fullmatch'", # Python >= 3.7 has 'Match' objects too ), ], ) def test_cli_too_many_functions(cli, err_msg): # Supplying multiple functions to writers that only cope with one result = run("hypothesis write " + cli) assert result.returncode == 2 assert "Error: " + err_msg in result.stderr assert ("Closest matches" in err_msg) == ("Closest matches" in result.stderr) CODE_TO_TEST = """ from typing import Sequence, List def sorter(seq: Sequence[int]) -> List[int]: return sorted(seq) """ def test_can_import_from_scripts_in_working_dir(tmp_path): (tmp_path / "mycode.py").write_text(CODE_TO_TEST, encoding="utf-8") result = run("hypothesis write mycode.sorter", cwd=tmp_path) assert result.returncode == 0 assert "Error: " not in result.stderr CLASS_CODE_TO_TEST = """ from typing import Sequence, List def my_func(seq: Sequence[int]) -> List[int]: return sorted(seq) class MyClass: @staticmethod def my_staticmethod(seq: Sequence[int]) -> List[int]: return sorted(seq) @classmethod def my_classmethod(cls, seq: Sequence[int]) -> List[int]: return sorted(seq) """ @pytest.mark.parametrize("func", ["my_staticmethod", "my_classmethod"]) def test_can_import_from_class(tmp_path, func): (tmp_path / "mycode.py").write_text(CLASS_CODE_TO_TEST, encoding="utf-8") result = run(f"hypothesis write mycode.MyClass.{func}", cwd=tmp_path) assert result.returncode == 0 assert "Error: " not in result.stderr @pytest.mark.parametrize( "classname,thing,kind", [ ("XX", "", "class"), ("MyClass", " and 'MyClass' class", "attribute"), ("my_func", " and 'my_func' attribute", "attribute"), ], ) def test_error_import_from_class(tmp_path, classname, thing, kind): (tmp_path / "mycode.py").write_text(CLASS_CODE_TO_TEST, encoding="utf-8") result = run(f"hypothesis write mycode.{classname}.XX", cwd=tmp_path) msg = f"Error: Found the 'mycode' module{thing}, but it doesn't have a 'XX' {kind}." assert result.returncode == 2 assert msg in result.stderr def test_magic_discovery_from_module(tmp_path): (tmp_path / "mycode.py").write_text(CLASS_CODE_TO_TEST, encoding="utf-8") result = run("hypothesis write mycode", cwd=tmp_path) assert result.returncode == 0 assert "my_func" in result.stdout assert "MyClass.my_staticmethod" in result.stdout assert "MyClass.my_classmethod" in result.stdout ROUNDTRIP_CODE_TO_TEST = """ from typing import Union import json def to_json(json: Union[dict,list]) -> str: return json.dumps(json) def from_json(json: str) -> Union[dict,list]: return json.loads(json) class MyClass: @staticmethod def to_json(json: Union[dict,list]) -> str: return json.dumps(json) @staticmethod def from_json(json: str) -> Union[dict,list]: return json.loads(json) class OtherClass: @classmethod def to_json(cls, json: Union[dict,list]) -> str: return json.dumps(json) @classmethod def from_json(cls, json: str) -> Union[dict,list]: return json.loads(json) """ def test_roundtrip_correct_pairs(tmp_path): (tmp_path / "mycode.py").write_text(ROUNDTRIP_CODE_TO_TEST, encoding="utf-8") result = run("hypothesis write mycode", cwd=tmp_path) assert result.returncode == 0 for scope1, scope2 in itertools.product( ["mycode.MyClass", "mycode.OtherClass", "mycode"], repeat=2 ): round_trip_code = f"""value0 = {scope1}.to_json(json=json) value1 = {scope2}.from_json(json=value0)""" if scope1 == scope2: assert round_trip_code in result.stdout else: assert round_trip_code not in result.stdout def test_empty_module_is_not_error(tmp_path): (tmp_path / "mycode.py").write_text("# Nothing to see here\n", encoding="utf-8") result = run("hypothesis write mycode", cwd=tmp_path) assert result.returncode == 0 assert "Error: " not in result.stderr assert "# Found no testable functions" in result.stdout ================================================ FILE: hypothesis-python/tests/ghostwriter/try-writing-for-installed.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Try running `hypothesis write ...` on all available modules. Prints a list of module names which caused some kind of internal error. The idea here is to check that we at least don't crash on anything that people actually ship, or at least only on the cases we know and don't really care about - there are a lot of strange things in a python install. Some have import-time side effects or errors so we skip them; others just have such weird semantics that we don't _want_ to support them. """ import multiprocessing import os import subprocess import sysconfig skip = ( "idlelib curses antigravity pip prompt_toolkit IPython .popen_ django. .test. " "execnet.script lib2to3.pgen2.conv tests. Cython. ~ - ._ libcst.codemod. " "modernize flask. sphinx. pyasn1 dbm.ndbm doctest" ).split() def getmodules(): std_lib = sysconfig.get_python_lib(standard_lib=True) for top, _, files in os.walk(std_lib): for nm in files: if nm.endswith(".py") and nm not in ("__init__.py", "__main__.py"): modname = ( os.path.join(top, nm)[len(std_lib) + 1 : -3] .replace(os.sep, ".") .replace("site-packages.", "") ) if not any(bad in modname for bad in skip): yield modname def write_for(mod): try: subprocess.run( ["hypothesis", "write", mod], check=True, capture_output=True, timeout=10, text=True, encoding="utf-8", ) except subprocess.SubprocessError as e: # Only report the error if we could load _but not process_ the module if ( "Error: Found the '" not in e.stderr and "Error: Failed to import" not in e.stderr ): return mod if __name__ == "__main__": print("# prints the names of modules for which `hypothesis write` errors out") with multiprocessing.Pool() as pool: for name in pool.imap(write_for, getmodules()): if name is not None: print(name, flush=True) ================================================ FILE: hypothesis-python/tests/lark/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/lark/test_grammar.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import json import pytest from lark.lark import Lark from hypothesis import given from hypothesis.errors import InvalidArgument from hypothesis.extra.lark import from_lark from hypothesis.strategies import characters, data, just from tests.common.debug import check_can_generate_examples, find_any # Adapted from the official Lark tutorial, with modifications to ensure # that the generated JSON is valid. i.e. no numbers starting with ".", # \f is not ignorable whitespace, and restricted strings only. Source: # https://github.com/lark-parser/lark/blob/master/docs/json_tutorial.md EBNF_GRAMMAR = r""" value: dict | list | STRING | NUMBER | "true" -> true | "false" -> false | "null" -> null list : "[" [value ("," value)*] "]" dict : "{" [STRING ":" value ("," STRING ":" value)*] "}" STRING : /"[a-z]*"/ NUMBER : /-?[1-9][0-9]*(\.[0-9]+)?([eE][+-]?[0-9]+)?/ WS : /[ \t\r\n]+/ %ignore WS """ LIST_GRAMMAR = r""" list : "[" [NUMBER ("," NUMBER)*] "]" NUMBER: /[0-9]|[1-9][0-9]*/ """ @given(from_lark(Lark(EBNF_GRAMMAR, start="value"))) def test_generates_valid_json(string): json.loads(string) @pytest.mark.parametrize( "start, type_", [ ("dict", dict), ("list", list), ("STRING", str), ("NUMBER", (int, float)), ("TRUE", bool), ("FALSE", bool), ("NULL", type(None)), ], ) @given(data=data()) def test_can_specify_start_rule(data, start, type_): string = data.draw(from_lark(Lark(EBNF_GRAMMAR, start="value"), start=start)) value = json.loads(string) assert isinstance(value, type_) def test_can_generate_ignored_tokens(): list_grammar = r""" list : "[" [STRING ("," STRING)*] "]" STRING : /"[a-z]*"/ WS : /[ \t\r\n]+/ %ignore WS """ strategy = from_lark(Lark(list_grammar, start="list")) # A JSON list of strings in canonical form which does not round-trip, # must contain ignorable whitespace in the initial string. find_any(strategy, lambda s: "\t" in s) def test_generation_without_whitespace(): find_any(from_lark(Lark(LIST_GRAMMAR, start="list")), lambda g: " " not in g) def test_cannot_convert_EBNF_to_strategy_directly(): with pytest.raises(InvalidArgument): # Not a Lark object check_can_generate_examples(from_lark(EBNF_GRAMMAR)) with pytest.raises(TypeError): # Not even the right number of arguments check_can_generate_examples(from_lark(EBNF_GRAMMAR, start="value")) with pytest.raises(InvalidArgument): # Wrong type for explicit_strategies check_can_generate_examples( from_lark(Lark(LIST_GRAMMAR, start="list"), explicit=[]) ) def test_required_undefined_terminals_require_explicit_strategies(): elem_grammar = r""" list : "[" ELEMENT ("," ELEMENT)* "]" %declare ELEMENT """ with pytest.raises(InvalidArgument, match=r"%declare"): check_can_generate_examples(from_lark(Lark(elem_grammar, start="list"))) strategy = {"ELEMENT": just("200")} check_can_generate_examples( from_lark(Lark(elem_grammar, start="list"), explicit=strategy) ) def test_cannot_use_explicit_strategies_for_unknown_terminals(): with pytest.raises(InvalidArgument): check_can_generate_examples( from_lark( Lark(LIST_GRAMMAR, start="list"), explicit={"unused_name": just("")} ) ) def test_non_string_explicit_strategies_are_invalid(): with pytest.raises(InvalidArgument): check_can_generate_examples( from_lark(Lark(LIST_GRAMMAR, start="list"), explicit={"NUMBER": just(0)}) ) @given( string=from_lark(Lark(LIST_GRAMMAR, start="list"), explicit={"NUMBER": just("0")}) ) def test_can_override_defined_terminal(string): assert sum(json.loads(string)) == 0 @given(string=from_lark(Lark(LIST_GRAMMAR, start="list"), alphabet="[0,]")) def test_can_generate_from_limited_alphabet(string): assert sum(json.loads(string)) == 0 @given(string=from_lark(Lark(LIST_GRAMMAR, start="list"), alphabet="[9]")) def test_can_generate_from_limited_alphabet_no_comma(string): assert len(json.loads(string)) <= 1 @given( string=from_lark( Lark(EBNF_GRAMMAR, start="value"), alphabet=characters(codec="ascii", exclude_characters=","), ) ) def test_can_generate_from_limited_alphabet_no_comma_json(string): assert "," not in string def test_error_if_alphabet_bans_all_start_rules(): with pytest.raises( InvalidArgument, match=r"No start rule .+ is allowed by alphabet=" ): check_can_generate_examples( from_lark(Lark(LIST_GRAMMAR, start="list"), alphabet="abc") ) ================================================ FILE: hypothesis-python/tests/nocover/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/nocover/test_argument_validation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import inspect from inspect import Parameter import pytest from hypothesis import strategies as st from hypothesis.strategies._internal.utils import _all_strategies from tests.common.arguments import argument_validation_test, e BAD_ARGS = [] def adjust(ex, **kwargs): f, a, b = ex b = dict(b) b.update(kwargs) BAD_ARGS.append((f, a, b)) for ex in [ e(st.lists, st.integers()), e(st.sets, st.integers()), e(st.frozensets, st.integers()), e(st.dictionaries, st.integers(), st.integers()), e(st.text), e(st.binary), ]: adjust(ex, min_size=-1) adjust(ex, max_size=-1) adjust(ex, min_size="no") adjust(ex, max_size="no") BAD_ARGS.extend([e(st.lists, st.nothing(), unique=True, min_size=1)]) test_raise_invalid_argument = argument_validation_test(BAD_ARGS) @pytest.mark.parametrize("name", sorted(_all_strategies)) def test_consistent_with_api_guide_on_kwonly_args(name): # Enforce our style-guide: if it has a default value, it should be # keyword-only, with a few exceptions. strategy = _all_strategies[name] for arg in inspect.signature(strategy).parameters.values(): assert ( arg.default == Parameter.empty or arg.kind != Parameter.POSITIONAL_OR_KEYWORD or arg.name in ("min_value", "max_value", "subtype_strategy", "columns") or name in ("text", "range_indexes", "badly_draw_lists", "write_pattern") ), f"need kwonly args in {name}" ================================================ FILE: hypothesis-python/tests/nocover/test_bad_repr.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st class BadRepr: def __init__(self, value): self.value = value def __repr__(self): return self.value Frosty = BadRepr("☃") def test_just_frosty(): assert repr(st.just(Frosty)) == "just(☃)" def test_sampling_snowmen(): assert repr(st.sampled_from((Frosty, "hi"))) == "sampled_from((☃, 'hi'))" def varargs(*args, **kwargs): pass @given( st.sampled_from( [ "✐", "✑", "✒", "✓", "✔", "✕", "✖", "✗", "✘", "✙", "✚", "✛", "✜", "✝", "✞", "✟", "✠", "✡", "✢", "✣", ] ) ) def test_sampled_from_bad_repr(c): pass ================================================ FILE: hypothesis-python/tests/nocover/test_baseexception.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given from hypothesis.errors import Flaky, FlakyFailure from hypothesis.strategies import composite, integers, none from tests.common.utils import Why, skipif_threading, xfail_on_crosshair @pytest.mark.parametrize( "e", [KeyboardInterrupt, SystemExit, GeneratorExit, ValueError] ) def test_exception_propagates_fine(e): @given(integers()) def test_raise(x): raise e with pytest.raises(e): test_raise() @pytest.mark.parametrize( "e", [KeyboardInterrupt, SystemExit, GeneratorExit, ValueError] ) def test_exception_propagates_fine_from_strategy(e): @composite def interrupt_eventually(draw): raise e # this line will not be executed, but must be here # to pass draw function static reference check return draw(none()) @given(interrupt_eventually()) def test_do_nothing(x): pass with pytest.raises(e): test_do_nothing() @xfail_on_crosshair(Why.other, strict=False) # extra replay from backend switch @pytest.mark.parametrize("e", [KeyboardInterrupt, ValueError]) def test_baseexception_no_rerun_no_flaky(e): runs = 0 interrupt = 3 @given(integers()) def test_raise_baseexception(x): nonlocal runs runs += 1 if runs == interrupt: raise e if issubclass(e, (KeyboardInterrupt, SystemExit, GeneratorExit)): # Here SystemExit and GeneratorExit are passed through with pytest.raises(e): test_raise_baseexception() assert runs == interrupt else: with pytest.raises(FlakyFailure): test_raise_baseexception() @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) # KI and GE only @pytest.mark.parametrize( "e", [KeyboardInterrupt, SystemExit, GeneratorExit, ValueError] ) def test_baseexception_in_strategy_no_rerun_no_flaky(e): runs = 0 interrupt = 3 @composite def interrupt_eventually(draw): nonlocal runs runs += 1 if runs == interrupt: raise e return draw(integers()) @given(interrupt_eventually()) def test_do_nothing(x): pass if issubclass(e, KeyboardInterrupt): with pytest.raises(e): test_do_nothing() assert runs == interrupt else: # Now SystemExit and GeneratorExit are caught like other exceptions with pytest.raises(Flaky): test_do_nothing() TEMPLATE = """ from hypothesis import given, note, strategies as st @st.composite def things(draw): raise {exception} # this line will not be executed, but must be here # to pass draw function static reference check return draw(st.none()) @given(st.data(), st.integers()) def test(data, x): if x > 100: data.draw({strategy}) raise {exception} """ @skipif_threading # something in pytest here is not thread safe @pytest.mark.parametrize("exc_name", ["SystemExit", "GeneratorExit"]) @pytest.mark.parametrize("use_composite", [True, False]) def test_explanations(testdir, exc_name, use_composite): code = TEMPLATE.format( exception=exc_name, strategy="things()" if use_composite else "st.none()" ) test_file = str(testdir.makepyfile(code)) pytest_stdout = str(testdir.runpytest_inprocess(test_file, "--tb=native").stdout) assert "x=101" in pytest_stdout assert exc_name in pytest_stdout ================================================ FILE: hypothesis-python/tests/nocover/test_boundary_exploration.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import HealthCheck, Verbosity, given, reject, settings, strategies as st from hypothesis.errors import Unsatisfiable from tests.common.debug import minimal from tests.common.utils import no_shrink @pytest.mark.parametrize("strat", [st.text(min_size=5)]) @settings(phases=no_shrink, deadline=None, suppress_health_check=list(HealthCheck)) @given(st.data()) def test_explore_arbitrary_function(strat, data): cache = {} def predicate(x): try: return cache[x] except KeyError: return cache.setdefault(x, data.draw(st.booleans(), label=repr(x))) try: minimal( strat, predicate, settings=settings( max_examples=10, database=None, verbosity=Verbosity.quiet ), ) except Unsatisfiable: reject() ================================================ FILE: hypothesis-python/tests/nocover/test_build_signature.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from inspect import signature from typing import get_type_hints from hypothesis import given, strategies as st from tests.common.debug import find_any def use_this_signature(self, a: int, b: list | None = None, *, x: float, y: str): pass class Model: # Emulates the implementation of Pydantic models. See e.g. # https://github.com/timothycrosley/hypothesis-auto/issues/10 __annotations__ = get_type_hints(use_this_signature) __signature__ = signature(use_this_signature) def __init__(self, **kwargs): # Check that we're being called with the expected arguments assert set(kwargs) == {"a", "x", "y"} assert isinstance(kwargs["a"], int) assert isinstance(kwargs["x"], float) assert isinstance(kwargs["y"], str) @given(st.builds(Model)) def test_builds_uses_signature_attribute(val): assert isinstance(val, Model) class ModelForFromType(Model): def __init__(self, **kwargs): assert set(kwargs) == {"a", "b", "x", "y"} self.b = kwargs["b"] assert self.b is None or isinstance(self.b, list) @given(st.from_type(ModelForFromType)) def test_from_type_uses_signature_attribute(val): assert isinstance(val, ModelForFromType) def test_from_type_can_be_default_or_annotation(): find_any(st.from_type(ModelForFromType), lambda m: m.b is None) find_any(st.from_type(ModelForFromType), lambda m: isinstance(m.b, list)) def use_annotations( self, test_a: int, test_b: str | None = None, *, test_x: float, test_y: str ): pass def use_signature( self, testA: int, testB: str | None = None, *, testX: float, testY: list[str] ): pass class ModelWithAlias: __annotations__ = get_type_hints(use_annotations) __signature__ = signature(use_signature) def __init__(self, **kwargs): # Check that we're being called with the expected arguments assert set(kwargs) == {"testA", "testX", "testY"} assert isinstance(kwargs["testA"], int) assert isinstance(kwargs["testX"], float) assert isinstance(kwargs["testY"], list) assert all(isinstance(elem, str) for elem in kwargs["testY"]) @given(st.builds(ModelWithAlias)) def test_build_using_different_signature_and_annotations(val): assert isinstance(val, ModelWithAlias) def use_bad_signature(self, testA: 1, *, testX: float): pass class ModelWithBadAliasSignature: __annotations__ = get_type_hints(use_annotations) __signature__ = signature(use_bad_signature) def __init__(self, **kwargs): assert set(kwargs) == {"testX"} assert isinstance(kwargs["testX"], float) @given(st.builds(ModelWithBadAliasSignature)) def test_build_with_non_types_in_signature(val): assert isinstance(val, ModelWithBadAliasSignature) class UnconventionalSignature: def __init__(x: int = 0, self: bool = True): # noqa assert not isinstance(x, int) x.self = self def test_build_in_from_type_with_self_named_something_else(): find_any(st.from_type(UnconventionalSignature), lambda x: x.self is True) find_any(st.from_type(UnconventionalSignature), lambda x: x.self is False) ================================================ FILE: hypothesis-python/tests/nocover/test_cache_implementation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import Counter from hypothesis import strategies as st from hypothesis.internal.cache import GenericCache from hypothesis.stateful import ( Bundle, RuleBasedStateMachine, initialize, invariant, rule, ) class CacheWithScores(GenericCache): def __init__(self, max_size): super().__init__(max_size) self.scores = {} def new_entry(self, key, value): return self.scores[key] class CacheRules(RuleBasedStateMachine): keys = Bundle("keys") @initialize(max_size=st.integers(1, 8)) def create_cache(self, max_size): self.cache = CacheWithScores(max_size) self.__values = {} self.__total_pins = 0 self.__pins = Counter() self.__live = set() self.__next_value = 0 self.__last_key = None def on_evict(evicted_key, value, score): assert self.__pins[evicted_key] == 0 assert score == self.cache.scores[evicted_key] assert value == self.__values[evicted_key] for k in self.cache: assert ( self.__pins[k] > 0 or self.cache.scores[k] >= score or k == self.__last_key ) self.cache.on_evict = on_evict @rule(key=st.integers(), score=st.integers(0, 100), target=keys) def new_key(self, key, score): if key not in self.cache.scores: self.cache.scores[key] = score return key @rule(key=keys) def set_key(self, key): if self.__total_pins < self.cache.max_size or key in self.cache: self.__last_key = key self.cache[key] = self.__next_value self.__values[key] = self.__next_value self.__next_value += 1 @invariant() def check_values(self): self.cache.check_valid() for key in self.cache: assert self.__values[key] == self.cache[key] @rule(key=keys) def pin_key(self, key): if key in self.cache: self.cache.pin(key, self.__values[key]) if self.__pins[key] == 0: self.__total_pins += 1 self.__pins[key] += 1 @rule(key=keys) def unpin_key(self, key): if self.__pins[key] > 0: self.cache.unpin(key) self.__pins[key] -= 1 if self.__pins[key] == 0: self.__total_pins -= 1 assert self.__total_pins >= 0 TestCache = CacheRules.TestCase ================================================ FILE: hypothesis-python/tests/nocover/test_cacheable.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import gc import weakref import pytest from hypothesis import given, settings, strategies as st from tests.common.utils import xfail_if_gil_disabled @pytest.mark.parametrize( "s", [ st.floats(), st.tuples(st.integers()), st.tuples(), st.one_of(st.integers(), st.text()), ], ) def test_is_cacheable(s): assert s.is_cacheable @pytest.mark.parametrize( "s", [ st.just([]), st.tuples(st.integers(), st.just([])), st.one_of(st.integers(), st.text(), st.just([])), ], ) def test_is_not_cacheable(s): assert not s.is_cacheable def test_non_cacheable_things_are_not_cached(): x = st.just([]) assert st.tuples(x) != st.tuples(x) def test_cacheable_things_are_cached(): x = st.just(()) assert st.tuples(x) == st.tuples(x) @xfail_if_gil_disabled def test_local_types_are_garbage_collected_issue_493(): store = None def run_locally(): class Test: @settings(database=None) @given(st.integers()) def test(self, i): pass nonlocal store store = weakref.ref(Test) Test().test() run_locally() del run_locally assert store() is not None gc.collect() assert store() is None ================================================ FILE: hypothesis-python/tests/nocover/test_characters.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import string from encodings.aliases import aliases import pytest from hypothesis import given, settings, strategies as st from tests.common.utils import Why, xfail_on_crosshair IDENTIFIER_CHARS = string.ascii_letters + string.digits + "_" @given(st.characters(exclude_characters=IDENTIFIER_CHARS)) def test_large_blacklist(c): assert c not in IDENTIFIER_CHARS @xfail_on_crosshair(Why.symbolic_outside_context) # seems like a crosshair bug here @given(st.data()) def test_arbitrary_blacklist(data): blacklist = data.draw(st.text(st.characters(max_codepoint=1000), min_size=1)) ords = list(map(ord, blacklist)) c = data.draw( st.characters( exclude_characters=blacklist, min_codepoint=max(0, min(ords) - 1), max_codepoint=max(0, max(ords) + 1), ) ) assert c not in blacklist def _enc(cdc): try: "".encode(cdc) return True except Exception: return False lots_of_encodings = sorted(x for x in set(aliases).union(aliases.values()) if _enc(x)) assert len(lots_of_encodings) > 100 # sanity-check @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes 2000s; large & slow symbolic strings", ) @given(data=st.data(), codec=st.sampled_from(lots_of_encodings)) @settings(max_examples=5) def test_can_constrain_characters_to_codec(data, codec): s = data.draw(st.text(st.characters(codec=codec), min_size=25)) s.encode(codec) ================================================ FILE: hypothesis-python/tests/nocover/test_collective_minimization.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import Phase, settings from hypothesis.errors import Unsatisfiable from hypothesis.strategies import lists from tests.common import standard_types from tests.common.debug import minimal @pytest.mark.parametrize("spec", standard_types, ids=repr) def test_can_collectively_minimize(spec): n = 10 try: xs = minimal( lists(spec, min_size=n, max_size=n), lambda x: len(set(map(repr, x))) >= 2, settings(max_examples=2000, phases=(Phase.generate, Phase.shrink)), ) assert len(xs) == n assert 2 <= len(set(map(repr, xs))) <= 3 except Unsatisfiable: pass ================================================ FILE: hypothesis-python/tests/nocover/test_compat.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from hypothesis import given, strategies as st from hypothesis.internal.compat import ceil, floor, int_from_bytes, int_to_bytes @given(st.binary()) def test_convert_back(bs): bs = bytearray(bs) assert int_to_bytes(int_from_bytes(bs), len(bs)) == bs bytes8 = st.builds(bytearray, st.binary(min_size=8, max_size=8)) @given(bytes8, bytes8) def test_to_int_in_big_endian_order(x, y): x, y = sorted((x, y)) assert 0 <= int_from_bytes(x) <= int_from_bytes(y) ints8 = st.integers(min_value=0, max_value=2**63 - 1) @given(ints8, ints8) def test_to_bytes_in_big_endian_order(x, y): x, y = sorted((x, y)) assert int_to_bytes(x, 8) <= int_to_bytes(y, 8) @given(st.fractions()) def test_ceil(x): assert isinstance(ceil(x), int) assert x <= ceil(x) < x + 1 assert ceil(x) == math.ceil(x) @given(st.fractions()) def test_floor(x): assert isinstance(floor(x), int) assert x - 1 < floor(x) <= x assert floor(x) == math.floor(x) ================================================ FILE: hypothesis-python/tests/nocover/test_completion.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st @given(st.data()) def test_never_draw_anything(data): pass ================================================ FILE: hypothesis-python/tests/nocover/test_complex_numbers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, strategies as st @pytest.mark.parametrize("width", [32, 64, 128]) @pytest.mark.parametrize("keyword", ["min_magnitude", "max_magnitude"]) @given(data=st.data()) def test_magnitude_validates(width, keyword, data): # See https://github.com/HypothesisWorks/hypothesis/issues/3573 component_width = width / 2 magnitude = data.draw( # 1.8 is a known example that hasn't validated in the past st.floats(0, width=component_width) | st.just(1.8), label=keyword, ) strat = st.complex_numbers(width=width, **{keyword: magnitude}) data.draw(strat) ================================================ FILE: hypothesis-python/tests/nocover/test_conjecture_engine.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, settings, strategies as st from hypothesis.database import InMemoryExampleDatabase, choices_from_bytes from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.conjecture.shrinker import Shrinker from tests.common.utils import ( Why, counts_calls, non_covering_examples, xfail_on_crosshair, ) from tests.conjecture.common import interesting_origin, run_to_nodes, shrinking_from @xfail_on_crosshair(Why.nested_given) def test_lot_of_dead_nodes(): @run_to_nodes def nodes(data): for i in range(4): if data.draw_integer(0, 2**7 - 1) != i: data.mark_invalid() data.mark_interesting(interesting_origin()) assert tuple(n.value for n in nodes) == (0, 1, 2, 3) def test_saves_data_while_shrinking(monkeypatch): key = b"hi there" n = 5 db = InMemoryExampleDatabase() assert list(db.fetch(key)) == [] seen = set() monkeypatch.setattr( ConjectureRunner, "generate_new_examples", lambda runner: runner.cached_test_function([bytes([255] * 10)]), ) def f(data): x = data.draw_bytes(10, 10) if sum(x) >= 2000 and len(seen) < n: seen.add(x) if x in seen: data.mark_interesting(interesting_origin()) runner = ConjectureRunner(f, settings=settings(database=db), database_key=key) runner.run() assert runner.interesting_examples assert len(seen) == n in_db = {choices_from_bytes(b)[0] for b in non_covering_examples(db)} assert in_db.issubset(seen) assert in_db == seen def test_can_discard(monkeypatch): n = 8 monkeypatch.setattr( ConjectureRunner, "generate_new_examples", lambda runner: runner.cached_test_function( tuple(bytes(v) for i in range(n) for v in [i, i]) ), ) @run_to_nodes def nodes(data): seen = set() while len(seen) < n: seen.add(data.draw_bytes()) data.mark_interesting(interesting_origin()) assert len(nodes) == n @xfail_on_crosshair(Why.nested_given) @given(st.integers(0, 255), st.integers(0, 255)) def test_cached_with_masked_byte_agrees_with_results(a, b): def f(data): data.draw_integer(0, 3) runner = ConjectureRunner(f) cached_a = runner.cached_test_function([a]) cached_b = runner.cached_test_function([b]) data_b = ConjectureData.for_choices([b], observer=runner.tree.new_observer()) runner.test_function(data_b) # If the cache found an old result, then it should match the real result. # If it did not, then it must be because A and B were different. assert (cached_a is cached_b) == (cached_a.nodes == data_b.nodes) def test_node_programs_fail_efficiently(monkeypatch): # Create 256 byte-sized nodes. None of the nodes can be deleted, and # every deletion attempt produces a different buffer. @shrinking_from(range(256)) def shrinker(data: ConjectureData): values = set() for _ in range(256): v = data.draw_integer(0, 2**8 - 1) values.add(v) if len(values) == 256: data.mark_interesting(interesting_origin()) monkeypatch.setattr( Shrinker, "run_node_program", counts_calls(Shrinker.run_node_program) ) shrinker.max_stall = 500 shrinker.fixate_shrink_passes([shrinker.node_program("XX")]) assert shrinker.shrinks == 0 assert 250 <= shrinker.calls <= 260 # The node program should have been run roughly 255 times, with a little # bit of wiggle room for implementation details. # - Too many calls mean that failing steps are doing too much work. # - Too few calls mean that this test is probably miscounting and buggy. assert 250 <= Shrinker.run_node_program.calls <= 260 ================================================ FILE: hypothesis-python/tests/nocover/test_conjecture_int_list.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import strategies as st from hypothesis.internal.conjecture.junkdrawer import IntList from hypothesis.stateful import RuleBasedStateMachine, initialize, invariant, rule INTEGERS = st.integers(0, 2**68) @st.composite def valid_index(draw): machine = draw(st.runner()) if not machine.model: return draw(st.nothing()) return draw(st.integers(0, len(machine.model) - 1)) class IntListRules(RuleBasedStateMachine): @initialize(ls=st.lists(INTEGERS)) def starting_lists(self, ls): self.model = list(ls) self.target = IntList(ls) @invariant() def lists_are_equivalent(self): if hasattr(self, "model"): assert isinstance(self.model, list) assert isinstance(self.target, IntList) assert len(self.model) == len(self.target) assert list(self.target) == self.model @rule(n=INTEGERS) def append(self, n): self.model.append(n) self.target.append(n) @rule(i=valid_index()) def delete(self, i): del self.model[i] del self.target[i] @rule(i=valid_index()) def agree_on_values(self, i): assert self.model[i] == self.target[i] TestIntList = IntListRules.TestCase ================================================ FILE: hypothesis-python/tests/nocover/test_conjecture_utils.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys from collections import Counter from hypothesis import assume, example, given, settings, strategies as st, target from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.engine import BUFFER_SIZE from hypothesis.internal.conjecture.provider_conformance import integer_weights from tests.conjecture.common import fresh_data @given(integer_weights(), st.randoms(use_true_random=True)) @settings(max_examples=3) def test_sampler_matches_distribution(weights, random): # if we randomly sample from Sampler(weights), the resulting distribution # should match the weights (to some tolerance). weights = weights.values() sampler = cu.Sampler(weights) counter = Counter() for _ in range(10_000): cd = fresh_data(random=random) n = sampler.sample(cd) counter[n] += 1 # if we ever pull in scipy to our test suite, we should do a chi squared # test here instead. expected = [w / sum(weights) for w in weights] counter_total = sum(counter.values()) # Counter.total() new in py3.10 actual = [counter[i] / counter_total for i in range(len(weights))] for p1, p2 in zip(expected, actual, strict=True): assert abs(p1 - p2) < 0.05, (expected, actual) @example(0, 1) @example(0, float("inf")) @example(cu.SMALLEST_POSITIVE_FLOAT, 2 * cu.SMALLEST_POSITIVE_FLOAT) @example(cu.SMALLEST_POSITIVE_FLOAT, 1) @example(cu.SMALLEST_POSITIVE_FLOAT, float("inf")) @example(sys.float_info.min, 1) @example(sys.float_info.min, float("inf")) @example(10, 10) @example(10, float("inf")) # BUFFER_SIZE divided by (2bytes coin + 0byte element) gives the # maximum number of elements that we would ever be able to generate. @given(st.floats(0, BUFFER_SIZE // 2), st.integers(0, BUFFER_SIZE // 2)) def test_p_continue(average_size, max_size): assume(average_size <= max_size) p = cu._calc_p_continue(average_size, max_size) assert 0 <= target(p, label="p") <= 1 assert 0 < target(p, label="-p") or average_size < 1e-5 abs_err = abs(average_size - cu._p_continue_to_avg(p, max_size)) assert target(abs_err, label="abs_err") < 0.01 @example(1.1, 10) @given(st.floats(0, 1), st.integers(0, BUFFER_SIZE // 2)) def test_p_continue_to_average(p_continue, max_size): average = cu._p_continue_to_avg(p_continue, max_size) assert 0 <= average <= max_size ================================================ FILE: hypothesis-python/tests/nocover/test_conventions.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.utils.conventions import UniqueIdentifier def test_unique_identifier_repr(): assert repr(UniqueIdentifier("hello_world")) == "hello_world" ================================================ FILE: hypothesis-python/tests/nocover/test_database_agreement.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import shutil import tempfile import pytest from hypothesis import settings, strategies as st from hypothesis.database import ( BackgroundWriteDatabase, DirectoryBasedExampleDatabase, InMemoryExampleDatabase, ) from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule class DatabaseComparison(RuleBasedStateMachine): def __init__(self): super().__init__() self.tempd = tempfile.mkdtemp() exampledir = os.path.join(self.tempd, "examples") self.dbs = [ InMemoryExampleDatabase(), DirectoryBasedExampleDatabase(exampledir), BackgroundWriteDatabase(InMemoryExampleDatabase()), ] keys = Bundle("keys") values = Bundle("values") @rule(target=keys, k=st.binary()) def k(self, k): return k @rule(target=values, v=st.binary()) def v(self, v): return v @rule(k=keys, v=values) def save(self, k, v): for db in self.dbs: db.save(k, v) @rule(k=keys, v=values) def delete(self, k, v): for db in self.dbs: db.delete(k, v) @rule(k1=keys, k2=keys, v=values) def move(self, k1, k2, v): for db in self.dbs: db.move(k1, k2, v) @rule(k=keys) def values_agree(self, k): last = None last_db = None for db in self.dbs: keys = set(db.fetch(k)) if last is not None: assert last == keys, (last_db, db) last = keys last_db = db def teardown(self): shutil.rmtree(self.tempd) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="isn't threadsafe" ) def test_database_equivalence(): DatabaseComparison.TestCase().runTest() ================================================ FILE: hypothesis-python/tests/nocover/test_database_usage.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import assume, core, find, given, settings, strategies as st from hypothesis.database import ( DirectoryBasedExampleDatabase, GitHubArtifactDatabase, InMemoryExampleDatabase, ReadOnlyDatabase, ) from hypothesis.errors import NoSuchExample, Unsatisfiable from tests.common.utils import ( Why, all_values, non_covering_examples, skipif_threading, xfail_on_crosshair, ) def has_a_non_zero_byte(x): return any(bytes(x)) def test_saves_incremental_steps_in_database(): key = b"a database key" database = InMemoryExampleDatabase() find( st.binary(min_size=10), has_a_non_zero_byte, settings=settings(database=database), database_key=key, ) assert len(all_values(database)) > 1 @xfail_on_crosshair(Why.symbolic_outside_context, strict=False) def test_clears_out_database_as_things_get_boring(): key = b"a database key" database = InMemoryExampleDatabase() do_we_care = True def stuff(): try: find( st.binary(min_size=50), lambda x: do_we_care and has_a_non_zero_byte(x), settings=settings(database=database, max_examples=10), database_key=key, ) except NoSuchExample: pass stuff() assert len(non_covering_examples(database)) > 1 do_we_care = False stuff() initial = len(non_covering_examples(database)) assert initial > 0 for _ in range(initial): stuff() keys = len(non_covering_examples(database)) if not keys: break else: raise AssertionError @xfail_on_crosshair(Why.other, strict=False) def test_trashes_invalid_examples(): database = InMemoryExampleDatabase() invalid = set() def stuff(): try: def condition(x): assume(x not in invalid) return not invalid and has_a_non_zero_byte(x) return find( st.binary(min_size=5), condition, settings=settings(database=database), database_key=b"a database key", ) except (Unsatisfiable, NoSuchExample): pass value = stuff() original = len(all_values(database)) assert original > 1 invalid.add(value) stuff() assert len(all_values(database)) < original @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="condition is easy for crosshair, stops early", ) def test_respects_max_examples_in_database_usage(): database = InMemoryExampleDatabase() do_we_care = True counter = 0 def check(x): nonlocal counter counter += 1 return do_we_care and has_a_non_zero_byte(x) def stuff(): try: find( st.binary(min_size=100), check, settings=settings(database=database, max_examples=10), database_key=b"a database key", ) except NoSuchExample: pass stuff() assert len(all_values(database)) > 10 do_we_care = False counter = 0 stuff() assert counter == 10 def test_does_not_use_database_when_seed_is_forced(monkeypatch): monkeypatch.setattr(core, "global_force_seed", 42) database = InMemoryExampleDatabase() database.fetch = None # type: ignore @settings(database=database) @given(st.integers()) def test(i): pass test() @skipif_threading # pytest .mktemp is not thread safe @given(st.binary(), st.binary()) def test_database_not_created_when_not_used(tmp_path_factory, key, value): path = tmp_path_factory.mktemp("hypothesis") / "examples" assert not path.exists() database = DirectoryBasedExampleDatabase(path) assert not list(database.fetch(key)) assert not path.exists() database.save(key, value) assert path.exists() assert list(database.fetch(key)) == [value] @skipif_threading def test_ga_database_not_created_when_not_used(tmp_path_factory): path = tmp_path_factory.mktemp("hypothesis") / "github-actions" assert not path.exists() ReadOnlyDatabase(GitHubArtifactDatabase("mock", "mock", path=path)) assert not path.exists() ================================================ FILE: hypothesis-python/tests/nocover/test_deferred_errors.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import find, given, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.strategies._internal.core import defines_strategy from tests.common.debug import check_can_generate_examples def test_does_not_error_on_initial_calculation(): st.floats(max_value=float("nan")) st.sampled_from([]) st.lists(st.integers(), min_size=5, max_size=2) st.floats(min_value=2.0, max_value=1.0) def test_errors_each_time(): s = st.integers(max_value=1, min_value=3) with pytest.raises(InvalidArgument): check_can_generate_examples(s) with pytest.raises(InvalidArgument): check_can_generate_examples(s) def test_errors_on_test_invocation(): @given(st.integers(max_value=1, min_value=3)) def test(x): pass with pytest.raises(InvalidArgument): test() def test_errors_on_find(): s = st.lists(st.integers(), min_size=5, max_size=2) with pytest.raises(InvalidArgument): find(s, lambda x: True) def test_errors_on_example(): s = st.floats(min_value=2.0, max_value=1.0) with pytest.raises(InvalidArgument): check_can_generate_examples(s) def test_does_not_recalculate_the_strategy(): calls = 0 @defines_strategy() def foo(): nonlocal calls calls += 1 return st.just(1) f = foo() assert calls == 0 check_can_generate_examples(f) assert calls == 1 check_can_generate_examples(f) assert calls == 1 ================================================ FILE: hypothesis-python/tests/nocover/test_drypython_returns.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from typing import Generic, TypeVar import pytest from hypothesis import given, strategies as st from hypothesis.errors import ResolutionFailed from tests.common.debug import check_can_generate_examples, find_any from tests.common.utils import temp_registered # Primitives: # =========== _InstanceType = TypeVar("_InstanceType", covariant=True) _TypeArgType1 = TypeVar("_TypeArgType1", covariant=True) _FirstType = TypeVar("_FirstType") _LawType = TypeVar("_LawType") class KindN(Generic[_InstanceType, _TypeArgType1]): pass class Lawful(Generic[_LawType]): """This type defines law-related operations.""" class MappableN(Generic[_FirstType], Lawful["MappableN[_FirstType]"]): """Behaves like a functor.""" # End definition: # =============== _ValueType = TypeVar("_ValueType") class MyFunctor(KindN["MyFunctor", _ValueType], MappableN[_ValueType]): def __init__(self, inner_value: _ValueType) -> None: self.inner_value = inner_value # Testing part: # ============= def target_func(mappable: "MappableN[_FirstType]") -> bool: return isinstance(mappable, MappableN) @given(st.data()) def test_my_mappable(source: st.DataObject) -> None: """ Checks that complex types with multiple inheritance levels and strings are fine. Regression test for https://github.com/HypothesisWorks/hypothesis/issues/3060 """ # In `returns` we register all types in `__mro__` # to be this exact type at the moment. But here, we only need `Mappable`. # Current `__mro__` is `MyFunctor / Kind / Mappable`: assert MyFunctor.__mro__[2] is MappableN with temp_registered( MyFunctor.__mro__[2], st.builds(MyFunctor), ): assert source.draw(st.builds(target_func)) is True A = TypeVar("A") B = TypeVar("B") C = TypeVar("C") D = TypeVar("D") class _FirstBase(Generic[A, B]): pass class _SecondBase(Generic[C, D]): pass # To be tested: class TwoGenericBases1(_FirstBase[A, B], _SecondBase[C, D]): pass class TwoGenericBases2(_FirstBase[C, D], _SecondBase[A, B]): pass class OneGenericOneConrete1(_FirstBase[int, str], _SecondBase[A, B]): pass class OneGenericOneConrete2(_FirstBase[A, B], _SecondBase[float, bool]): pass class MixedGenerics1(_FirstBase[int, B], _SecondBase[C, bool]): pass class MixedGenerics2(_FirstBase[A, str], _SecondBase[float, D]): pass class AllConcrete(_FirstBase[int, str], _SecondBase[float, bool]): pass _generic_test_types = ( TwoGenericBases1, TwoGenericBases2, OneGenericOneConrete1, OneGenericOneConrete2, MixedGenerics1, MixedGenerics2, AllConcrete, ) @pytest.mark.parametrize("type_", _generic_test_types) def test_several_generic_bases(type_): with temp_registered(_FirstBase, st.builds(type_)): find_any(st.builds(_FirstBase)) with temp_registered(_SecondBase, st.builds(type_)): find_any(st.builds(_SecondBase)) def var_generic_func1(obj: _FirstBase[A, B]): pass def var_generic_func2(obj: _SecondBase[A, B]): pass def concrete_generic_func1(obj: _FirstBase[int, str]): pass def concrete_generic_func2(obj: _SecondBase[float, bool]): pass def mixed_generic_func1(obj: _FirstBase[A, str]): pass def mixed_generic_func2(obj: _SecondBase[float, D]): pass @pytest.mark.parametrize("type_", _generic_test_types) @pytest.mark.parametrize( "func", [ var_generic_func1, var_generic_func2, concrete_generic_func1, concrete_generic_func2, mixed_generic_func1, mixed_generic_func2, ], ) def test_several_generic_bases_functions(type_, func): with ( temp_registered(_FirstBase, st.builds(type_)), temp_registered(_SecondBase, st.builds(type_)), ): find_any(st.builds(func)) with temp_registered(type_, st.builds(type_)): find_any(st.builds(func)) def wrong_generic_func1(obj: _FirstBase[A, None]): pass def wrong_generic_func2(obj: _SecondBase[None, bool]): pass @pytest.mark.parametrize("func", [wrong_generic_func1, wrong_generic_func2]) def test_several_generic_bases_wrong_functions(func): with ( temp_registered(AllConcrete, st.builds(AllConcrete)), pytest.raises(ResolutionFailed), ): check_can_generate_examples(st.builds(func)) ================================================ FILE: hypothesis-python/tests/nocover/test_duplication.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import Counter import pytest from hypothesis import given, settings from hypothesis.strategies._internal import SearchStrategy from tests.common.utils import Why, xfail_on_crosshair class Blocks(SearchStrategy): def __init__(self, n): super().__init__() self.n = n def do_draw(self, data): return data.draw_bytes(self.n, self.n) @xfail_on_crosshair(Why.symbolic_outside_context) @pytest.mark.parametrize("n", range(1, 5)) def test_does_not_duplicate_blocks(n): counts = Counter() @given(Blocks(n)) @settings(database=None) def test(b): counts[b] += 1 test() assert set(counts.values()) == {1} @xfail_on_crosshair(Why.other, strict=False) # CrosshairInternal for n>0 @pytest.mark.parametrize("n", range(1, 5)) def test_mostly_does_not_duplicate_blocks_even_when_failing(n): counts = Counter() @settings(database=None) @given(Blocks(n)) def test(b): counts[b] += 1 if len(counts) > 3: raise ValueError try: test() except ValueError: pass # There are two circumstances in which a duplicate is allowed: We replay # the failing test once to check for flakiness, and then we replay the # fully minimized failing test at the end to display the error. The # complication comes from the fact that these may or may not be the same # test case, so we can see either two test cases each run twice or one # test case which has been run three times. assert set(counts.values()) in ({1, 2}, {1, 3}) assert len([k for k, v in counts.items() if v > 1]) <= 2 ================================================ FILE: hypothesis-python/tests/nocover/test_dynamic_variable.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.utils.dynamicvariables import DynamicVariable def test_can_assign(): d = DynamicVariable(1) assert d.value == 1 with d.with_value(2): assert d.value == 2 assert d.value == 1 def test_can_nest(): d = DynamicVariable(1) with d.with_value(2): assert d.value == 2 with d.with_value(3): assert d.value == 3 assert d.value == 2 assert d.value == 1 ================================================ FILE: hypothesis-python/tests/nocover/test_emails.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, settings from hypothesis.strategies import emails, just @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~7 mins; first barrier: the `sampled_from(get_top_level_domains())` decision is " "realized via iterative comparisons; see https://github.com/pschanely/CrossHair/issues/332", ) @given(emails()) def test_is_valid_email(address: str): local, at_, domain = address.rpartition("@") assert len(address) <= 254 assert at_ == "@" assert local assert domain assert not domain.lower().endswith(".arpa") @given(emails(domains=just("mydomain.com"))) def test_can_restrict_email_domains(address: str): assert address.endswith("@mydomain.com") ================================================ FILE: hypothesis-python/tests/nocover/test_eval_as_source.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.internal.reflection import source_exec_as_module def test_can_eval_as_source(): assert source_exec_as_module("foo=1").foo == 1 def test_caches(): x = source_exec_as_module("foo=2") y = source_exec_as_module("foo=2") assert x is y RECURSIVE = """ from hypothesis.internal.reflection import source_exec_as_module def test_recurse(): assert not ( source_exec_as_module("too_much_recursion = False").too_much_recursion) """ def test_can_call_self_recursively(): source_exec_as_module(RECURSIVE).test_recurse() ================================================ FILE: hypothesis-python/tests/nocover/test_exceptiongroup.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import asyncio import sys from collections.abc import Callable import pytest from hypothesis import errors, given, reject, strategies as st from hypothesis.internal.compat import ExceptionGroup from hypothesis.strategies import DataObject # this file is not typechecked by mypy, which only runs py310 if sys.version_info < (3, 11): pytest.skip("asyncio.TaskGroup not available on None: @given(st.data()) def test_function(data: DataObject) -> None: async def task(pred: Callable[[bool], bool]) -> None: data.draw(st.booleans().filter(pred)) async def _main() -> None: async with asyncio.TaskGroup(), asyncio.TaskGroup() as tg2: tg2.create_task(task(bool)) tg2.create_task(task(lambda _: False)) asyncio.run(_main()) with pytest.raises(errors.Unsatisfiable): test_function() def test_exceptiongroup_user_originated() -> None: @given(st.data()) def test_function(data): raise ExceptionGroup("foo", [ValueError(), ValueError()]) with pytest.raises(ExceptionGroup) as exc_info: test_function() e = exc_info.value assert e.message == "foo" assert isinstance(e, ExceptionGroup) assert len(e.exceptions) == 2 assert all(isinstance(child_e, ValueError) for child_e in e.exceptions) @given(st.data()) def test_single_exc_group(data): raise ExceptionGroup("important message for user", [ValueError()]) with pytest.raises(ExceptionGroup) as exc_info: test_single_exc_group() e = exc_info.value assert e.message == "important message for user" assert isinstance(e, ExceptionGroup) assert len(e.exceptions) == 1 assert isinstance(e.exceptions[0], ValueError) def test_exceptiongroup_multiple_stop() -> None: @given(st.data()) def test_function(data): async def task(d: DataObject) -> None: d.conjecture_data.mark_invalid() async def _main(d: DataObject) -> None: async with asyncio.TaskGroup() as tg: tg.create_task(task(d)) tg.create_task(task(d)) asyncio.run(_main(data)) # multiple stoptests -> single stoptest -> unsatisfiable with pytest.raises(errors.Unsatisfiable): test_function() def test_exceptiongroup_stop_and_hypothesisexception() -> None: # group with stoptest+invalidargument -> invalidargument @given(st.data()) def test_function(data): async def task_stoptest(d: DataObject) -> None: # only mark some runs as invalid to not raise Unsatisfiable if d.draw(st.integers(min_value=0, max_value=1)) == 1: d.conjecture_data.mark_invalid() async def task_invalid_argument(d: DataObject) -> None: d.draw(st.integers(max_value=2, min_value=3)) async def _main(d: DataObject) -> None: async with asyncio.TaskGroup() as tg: tg.create_task(task_stoptest(d)) tg.create_task(task_invalid_argument(d)) asyncio.run(_main(data)) with pytest.raises(errors.InvalidArgument): test_function() def test_exceptiongroup_multiple_hypothesisexception() -> None: # multiple UnsatisfiedAssumption => first one is reraised => engine suppresses it @given(st.integers(min_value=0, max_value=3)) def test_function(val: int) -> None: async def task(value: int) -> None: if value == 0: reject() async def _main(value: int) -> None: async with asyncio.TaskGroup() as tg: tg.create_task(task(value)) tg.create_task(task(value)) asyncio.run(_main(val)) test_function() def test_exceptiongroup_multiple_InvalidArgument() -> None: # multiple InvalidArgument => only first one is reraised... which seems bad. # But raising a group might break ghostwriter(?) @given(st.data()) def test_function(data: DataObject) -> None: async def task1(d: DataObject) -> None: d.draw(st.integers(max_value=1, min_value=3)) async def task2(d: DataObject) -> None: d.draw(st.integers(max_value=2, min_value=3)) async def _main(d: DataObject) -> None: async with asyncio.TaskGroup() as tg: tg.create_task(task1(d)) tg.create_task(task2(d)) asyncio.run(_main(data)) with pytest.raises(errors.InvalidArgument): test_function() ================================================ FILE: hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import dataclass, field from random import Random from typing import Any import pytest from hypothesis import ( HealthCheck, Phase, Verbosity, assume, given, note, settings, strategies as st, ) from hypothesis.internal.conjecture.data import Status from hypothesis.internal.conjecture.engine import ConjectureRunner from tests.common.utils import Why, xfail_on_crosshair from tests.conjecture.common import interesting_origin @dataclass class Write: value: Any child: Any @dataclass class Branch: bits: Any children: Any = field(default_factory=dict) @dataclass class Terminal: status: Any payload: Any = field(default=None) nodes = st.deferred(lambda: terminals | writes | branches) # Does not include Status.OVERRUN by design: That happens because of the size # of the string, not the input language. terminals = st.one_of( st.just(Terminal(Status.VALID)), st.just(Terminal(Status.INVALID)), st.builds(Terminal, status=st.just(Status.INTERESTING), payload=st.integers(0, 10)), ) branches = st.builds(Branch, bits=st.integers(1, 64)) writes = st.builds(Write, value=st.binary(min_size=1), child=nodes) # Remember what the default phases are with no test running, so that we can # run an outer test with non-default phases and then restore the defaults for # the inner test. _default_phases = settings.default.phases def run_language_test_for(root, data, seed): def test(local_data): node = root while not isinstance(node, Terminal): if isinstance(node, Write): local_data.draw_bytes( len(node.value), len(node.value), forced=node.value ) node = node.child else: assert isinstance(node, Branch) c = local_data.draw_integer(0, 2**node.bits - 1) try: node = node.children[c] except KeyError: if data is None: return node = node.children.setdefault(c, data.draw(nodes)) assert isinstance(node, Terminal) if node.status == Status.INTERESTING: local_data.mark_interesting(interesting_origin(node.payload)) elif node.status == Status.INVALID: local_data.mark_invalid() runner = ConjectureRunner( test, settings=settings( max_examples=1, database=None, suppress_health_check=list(HealthCheck), verbosity=Verbosity.quiet, # Restore the global default phases, so that we don't inherit the # phases setting from the outer test. phases=_default_phases, ), random=Random(seed), ) try: runner.run() finally: if data is not None: note(root) assume(runner.interesting_examples) # technically nested-engine, but same problem @xfail_on_crosshair(Why.nested_given, strict=False) @settings( suppress_health_check=list(HealthCheck), deadline=None, phases=set(settings.default.phases) - {Phase.shrink}, ) @given(st.data()) def test_explore_an_arbitrary_language(data): root = data.draw(writes | branches) seed = data.draw(st.integers(0, 2**64 - 1)) run_language_test_for(root, data, seed) @pytest.mark.parametrize("seed, language", []) def test_run_specific_example(seed, language): """This test recreates individual languages generated with the main test. These are typically manually pruned down a bit - e.g. it's OK to remove VALID nodes because KeyError is treated as if it lead to one in this test (but not in the @given test). These tests are likely to be fairly fragile with respect to changes in the underlying engine. Feel free to delete examples if they start failing after a change. """ run_language_test_for(language, None, seed) ================================================ FILE: hypothesis-python/tests/nocover/test_fancy_repr.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import strategies as st def test_floats_is_floats(): assert repr(st.floats()) == "floats()" def test_includes_non_default_values(): assert repr(st.floats(max_value=1.0)) == "floats(max_value=1.0)" def foo(*args, **kwargs): pass # fmt: off # The linebreaks here can force our lambda repr code into specific paths, # so we tell Black to leave them as-is. def test_builds_repr(): assert repr(st.builds(foo, st.just(1), x=st.just(10))) == \ 'builds(foo, just(1), x=just(10))' def test_map_repr(): assert repr(st.integers().map(abs)) == 'integers().map(abs)' assert repr(st.integers().map(lambda x: x * 2)) == \ 'integers().map(lambda x: x * 2)' def test_filter_repr(): assert repr(st.integers().filter(lambda x: x != 3)) == \ 'integers().filter(lambda x: x != 3)' def test_flatmap_repr(): assert repr(st.integers().flatmap(lambda x: st.booleans())) == \ 'integers().flatmap(lambda x: st.booleans())' ================================================ FILE: hypothesis-python/tests/nocover/test_filtering.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, strategies as st from hypothesis.strategies import integers, lists @pytest.mark.parametrize( ("specifier", "condition"), [(integers(), lambda x: x > 1), (lists(integers()), bool)], ) def test_filter_correctly(specifier, condition): @given(specifier.filter(condition)) def test_is_filtered(x): assert condition(x) test_is_filtered() # A variety of strategies that generate the integers 1-20 inclusive, but might # differ in their support for special-case filtering. one_to_twenty_strategies = [ st.integers(1, 20), st.integers(0, 19).map(lambda x: x + 1), st.sampled_from(range(1, 21)), st.sampled_from(range(20)).map(lambda x: x + 1), ] @pytest.mark.parametrize("base", one_to_twenty_strategies) @given( data=st.data(), forbidden_values=st.lists(st.integers(1, 20), max_size=19, unique=True), ) def test_chained_filters_agree(data, forbidden_values, base): def forbid(s, forbidden): """Helper function to avoid Python variable scoping issues.""" return s.filter(lambda x: x != forbidden) s = base for forbidden in forbidden_values: s = forbid(s, forbidden) x = data.draw(s) assert 1 <= x <= 20 assert x not in forbidden_values @pytest.mark.parametrize("base", one_to_twenty_strategies) def test_chained_filters_repr(base): def foo(x): return x != 0 def bar(x): return x != 2 filtered = base.filter(foo) chained = filtered.filter(bar) assert repr(chained) == f"{base!r}.filter(foo).filter(bar)" assert repr(filtered) == f"{base!r}.filter(foo)" ================================================ FILE: hypothesis-python/tests/nocover/test_find.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import pytest from hypothesis import find, settings as Settings from hypothesis.errors import NoSuchExample from hypothesis.strategies import booleans, dictionaries, floats, integers, lists from tests.common.debug import minimal def test_can_find_an_int(): assert minimal(integers()) == 0 assert minimal(integers(), lambda x: x >= 13) == 13 def test_can_find_list(): x = minimal(lists(integers()), lambda x: sum(x) >= 10) assert sum(x) == 10 def test_can_find_nan(): minimal(floats(), math.isnan) def test_can_find_nans(): x = minimal(lists(floats()), lambda x: math.isnan(sum(x))) if len(x) == 1: assert math.isnan(x[0]) else: assert 2 <= len(x) <= 3 def test_condition_is_name(): settings = Settings(max_examples=20) with pytest.raises(NoSuchExample) as e: find(booleans(), lambda x: False, settings=settings) assert "lambda x:" in e.value.args[0] with pytest.raises(NoSuchExample) as e: find(integers(), lambda x: "☃" in str(x), settings=settings) assert "lambda x:" in e.value.args[0] def bad(x): return False with pytest.raises(NoSuchExample) as e: find(integers(), bad, settings=settings) assert "bad" in e.value.args[0] def test_find_dictionary(): smallest = minimal( dictionaries(keys=integers(), values=integers()), lambda xs: any(kv[0] > kv[1] for kv in xs.items()), ) assert len(smallest) == 1 ================================================ FILE: hypothesis-python/tests/nocover/test_fixtures.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import time from tests.common import TIME_INCREMENT from tests.common.utils import skipif_time_unpatched @skipif_time_unpatched def test_time_consistently_increments_in_tests(): x = time.time() y = time.time() z = time.time() assert y == x + TIME_INCREMENT assert z == y + TIME_INCREMENT ================================================ FILE: hypothesis-python/tests/nocover/test_flatmap.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import Counter import pytest from hypothesis import HealthCheck, assume, given, settings from hypothesis.database import InMemoryExampleDatabase from hypothesis.strategies import ( booleans, builds, floats, integers, just, lists, text, tuples, ) from tests.common.debug import find_any, minimal from tests.common.utils import Why, xfail_on_crosshair ConstantLists = integers().flatmap(lambda i: lists(just(i))) OrderedPairs = integers(1, 200).flatmap(lambda e: tuples(integers(0, e - 1), just(e))) # This health check fails very very occasionally - rarely enough to not be worth # investigation @settings(max_examples=100, suppress_health_check=[HealthCheck.filter_too_much]) @given(ConstantLists) def test_constant_lists_are_constant(x): assume(len(x) >= 3) assert len(set(x)) == 1 @settings(max_examples=100) @given(OrderedPairs) def test_in_order(x): assert x[0] < x[1] @xfail_on_crosshair( Why.undiscovered ) # (SampledFromStrategy.calc_label() hashes a symbolic float) def test_flatmap_retrieve_from_db(): track = [] @given(floats(0, 1).flatmap(lambda x: lists(just(x)))) @settings(database=InMemoryExampleDatabase()) def record_and_test_size(xs): if sum(xs) >= 1: track.append(xs) raise AssertionError with pytest.raises(AssertionError): record_and_test_size() assert track example = track[-1] track = [] with pytest.raises(AssertionError): record_and_test_size() assert track[0] == example def test_flatmap_does_not_reuse_strategies(): s = builds(list).flatmap(just) assert find_any(s) is not find_any(s) def test_flatmap_has_original_strategy_repr(): ints = integers() ints_up = ints.flatmap(lambda n: integers(min_value=n)) assert repr(ints) in repr(ints_up) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~6 mins in CI, but ~7 sec in isolation. Unsure why", ) def test_mixed_list_flatmap(): s = lists(booleans().flatmap(lambda b: booleans() if b else text())) def criterion(ls): c = Counter(type(l) for l in ls) return len(c) >= 2 and min(c.values()) >= 3 result = minimal(s, criterion) assert len(result) == 6 assert set(result) == {False, ""} @xfail_on_crosshair(Why.undiscovered) # for n >= 8 at least @pytest.mark.parametrize("n", range(1, 10)) def test_can_shrink_through_a_binding(n): bool_lists = integers(0, 100).flatmap( lambda k: lists(booleans(), min_size=k, max_size=k) ) assert minimal(bool_lists, lambda x: x.count(True) >= n) == [True] * n @xfail_on_crosshair(Why.undiscovered) # for n >= 8 at least @pytest.mark.parametrize("n", range(1, 10)) def test_can_delete_in_middle_of_a_binding(n): bool_lists = integers(1, 100).flatmap( lambda k: lists(booleans(), min_size=k, max_size=k) ) result = minimal(bool_lists, lambda x: x[0] and x[-1] and x.count(False) >= n) assert result == [True] + [False] * n + [True] ================================================ FILE: hypothesis-python/tests/nocover/test_floating.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Tests for being able to generate weird and wonderful floating point numbers.""" import math import sys import pytest from hypothesis import HealthCheck, assume, given, settings from hypothesis.internal.floats import float_to_int from hypothesis.strategies import data, floats, lists from tests.common.debug import find_any from tests.common.utils import fails TRY_HARDER = settings( max_examples=1000, suppress_health_check=[HealthCheck.filter_too_much] ) @given(floats()) @TRY_HARDER def test_is_float(x): assert isinstance(x, float) @fails @given(floats()) @TRY_HARDER def test_inversion_is_imperfect(x): assume(x != 0.0) y = 1.0 / x assert x * y == 1.0 @given(floats(-sys.float_info.max, sys.float_info.max)) def test_largest_range(x): assert not math.isinf(x) @given(floats()) @TRY_HARDER def test_negation_is_self_inverse(x): assume(not math.isnan(x)) y = -x assert -y == x @fails @given(lists(floats())) @TRY_HARDER def test_is_not_nan(xs): assert not any(math.isnan(x) for x in xs) @fails @given(floats()) @TRY_HARDER def test_is_not_positive_infinite(x): assume(x > 0) assert not math.isinf(x) @fails @given(floats()) @TRY_HARDER def test_is_not_negative_infinite(x): assume(x < 0) assert not math.isinf(x) @fails @given(floats()) @TRY_HARDER def test_is_int(x): assume(math.isfinite(x)) assert x == int(x) @fails @given(floats()) @TRY_HARDER def test_is_not_int(x): assume(math.isfinite(x)) assert x != int(x) @fails @given(floats()) @TRY_HARDER def test_is_in_exact_int_range(x): assume(math.isfinite(x)) assert x + 1 != x @fails @given(floats()) @TRY_HARDER def test_can_find_floats_that_do_not_round_trip_through_strings(x): assert float(str(x)) == x @fails @given(floats()) @TRY_HARDER def test_can_find_floats_that_do_not_round_trip_through_reprs(x): assert float(repr(x)) == x finite_floats = floats(allow_infinity=False, allow_nan=False) @settings(deadline=None) @given(finite_floats, finite_floats, data()) def test_floats_are_in_range(x, y, data): x, y = sorted((x, y)) assume(x < y) t = data.draw(floats(x, y)) assert x <= t <= y @pytest.mark.parametrize("neg", [False, True]) @pytest.mark.parametrize("snan", [False, True]) def test_can_find_negative_and_signaling_nans(neg, snan): find_any( floats().filter(math.isnan), lambda x: ( snan is (float_to_int(abs(x)) != float_to_int(float("nan"))) and neg is (math.copysign(1, x) == -1) ), settings=TRY_HARDER, ) ================================================ FILE: hypothesis-python/tests/nocover/test_from_type_recipe.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st from hypothesis.strategies._internal.types import _global_type_lookup from tests.common.debug import find_any TYPES = sorted( ( x for x in _global_type_lookup if x.__module__ != "typing" and x.__name__ != "ByteString" ), key=str, ) def everything_except(excluded_types): """Recipe copied from the docstring of ``from_type``""" return ( st.from_type(type) .flatmap(st.from_type) .filter(lambda x: not isinstance(x, excluded_types)) ) @given( excluded_types=st.lists( st.sampled_from(TYPES), min_size=1, max_size=3, unique=True ).map(tuple), data=st.data(), ) def test_recipe_for_everything_except(excluded_types, data): value = data.draw(everything_except(excluded_types)) assert not isinstance(value, excluded_types) def test_issue_4144_regression(): find_any(everything_except(()), lambda t: t is not type) ================================================ FILE: hypothesis-python/tests/nocover/test_given_error_conditions.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import HealthCheck, given, reject, settings from hypothesis.errors import Unsatisfiable from hypothesis.strategies import integers from tests.common.utils import fails_with @fails_with(Unsatisfiable) @given(integers()) @settings(max_examples=50, suppress_health_check=list(HealthCheck)) def test_raises_unsatisfiable_if_all_false(x): reject() ================================================ FILE: hypothesis-python/tests/nocover/test_given_reuse.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, strategies as st given_booleans = given(st.booleans()) @given_booleans def test_has_an_arg_named_x(x): pass @given_booleans def test_has_an_arg_named_y(y): pass given_named_booleans = given(z=st.text()) def test_fail_independently(): @given_named_booleans def test_z1(z): raise AssertionError @given_named_booleans def test_z2(z): pass with pytest.raises(AssertionError): test_z1() test_z2() ================================================ FILE: hypothesis-python/tests/nocover/test_health_checks.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import time import pytest from pytest import raises from hypothesis import HealthCheck, Phase, given, seed, settings, strategies as st from hypothesis.errors import FailedHealthCheck from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.engine import BUFFER_SIZE from hypothesis.strategies._internal.lazy import LazyStrategy pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="slow - large number of symbolics", ) large_strategy = st.binary(min_size=7000, max_size=7000) too_large_strategy = st.tuples(large_strategy, large_strategy) def test_large_data_will_fail_a_health_check(): @given(st.none() | too_large_strategy) @settings(database=None) def test(x): pass with raises(FailedHealthCheck) as e: test() assert "maximum allowed entropy" in e.value.args[0] def test_large_base_example_fails_health_check(): @given(large_strategy) def test(b): pass with pytest.raises(FailedHealthCheck) as exc: test() assert str(HealthCheck.large_base_example) in str(exc.value) def test_example_that_shrinks_to_overrun_fails_health_check(): @given(too_large_strategy | st.none()) def test(b): pass with pytest.raises(FailedHealthCheck) as exc: test() assert str(HealthCheck.large_base_example) in str(exc.value) slow_down_init = True def slow_init_integers(*args, **kwargs): # This mimics st.characters() or st.text(), which perform some # expensive Unicode calculations when the cache is empty. global slow_down_init if slow_down_init: time.sleep(0.5) # We monkeypatch time, so this is fast slow_down_init = False return st.integers(*args, **kwargs) @given(st.data()) def test_lazy_slow_initialization_issue_2108_regression(data): # Slow init in strategies wrapped in a LazyStrategy, inside an interactive draw, # should be attributed to drawing from the strategy (not the test function). # Specifically, this used to fail with a DeadlineExceeded error. data.draw(LazyStrategy(slow_init_integers, (), {})) def test_does_not_trigger_health_check_on_simple_strategies(monkeypatch): existing_draw = ConjectureData.draw_integer # We need to make drawing data artificially slow in order to trigger this # effect. This isn't actually slow because time is fake in our CI, but # we need it to pretend to be. def draw_integer(*args, **kwargs): time.sleep(0.001) return existing_draw(*args, **kwargs) monkeypatch.setattr(ConjectureData, "draw_integer", draw_integer) for _ in range(100): # Setting max_examples=11 ensures we have enough examples for the # health checks to finish running, but cuts the generation short # after that point to allow this test to run in reasonable time. @settings(database=None, max_examples=11, phases=[Phase.generate]) @given(st.integers()) def test(n): pass test() def test_does_not_trigger_health_check_when_most_examples_are_small(): for i in range(10): @seed(i) # Setting max_examples=11 ensures we have enough examples for the # health checks to finish running, but cuts the generation short # after that point to allow this test to run in reasonable time. @settings(database=None, max_examples=11, phases=[Phase.generate]) @given( st.integers(0, 100).flatmap( lambda n: st.binary( min_size=min(n * 100, BUFFER_SIZE), max_size=n * 100 ) ) ) def test(b): pass test() ================================================ FILE: hypothesis-python/tests/nocover/test_imports.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import * from hypothesis.strategies import * def test_can_star_import_from_hypothesis(): find( lists(integers()), lambda x: sum(x) > 1, settings=settings(max_examples=10000, verbosity=Verbosity.quiet), ) ================================================ FILE: hypothesis-python/tests/nocover/test_integer_ranges.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, settings from hypothesis.strategies import integers from tests.common.utils import Why, xfail_on_crosshair @xfail_on_crosshair(Why.symbolic_outside_context) def test_bounded_integers_distribution_of_bit_width_issue_1387_regression(): values = [] @settings(database=None, max_examples=1000) @given(integers(0, 1e100)) def test(x): if 2 <= x <= int(1e100) - 2: # skip forced-endpoints values.append(x) test() # We draw from a shaped distribution up to 128bit ~7/8 of the time, and # uniformly the rest. So we should get some very large but not too many. huge = sum(x > 1e97 for x in values) assert huge != 0 or len(values) < 800 assert huge <= 0.5 * len(values) # expected ~1/8 ================================================ FILE: hypothesis-python/tests/nocover/test_interesting_origin.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, settings, strategies as st from hypothesis.internal.compat import ExceptionGroup from tests.common.utils import flaky def go_wrong_naive(a, b): try: assert a + b < 100 a / b except Exception: # Hiding the actual problem is terrible, but this pattern can make sense # if you're raising a library-specific or semantically meaningful error. raise ValueError("Something went wrong") # noqa def go_wrong_with_cause(a, b): try: assert a + b < 100 a / b except Exception as err: # Explicit chaining is the *right way* to change exception type. raise ValueError("Something went wrong") from err def go_wrong_coverup(a, b): try: assert a + b < 100 a / b except Exception: # This pattern SHOULD be local enough that it never distinguishes # errors in practice... but if it does, we're ready. raise ValueError("Something went wrong") from None @pytest.mark.parametrize( "function", [go_wrong_naive, go_wrong_with_cause, go_wrong_coverup], ids=lambda f: f.__name__, ) @flaky(max_runs=3, min_passes=1) def test_can_generate_specified_version(function): @given(st.integers(), st.integers()) @settings(database=None, report_multiple_bugs=True) def test_fn(x, y): # Indirection to fix https://github.com/HypothesisWorks/hypothesis/issues/2888 return function(x, y) with pytest.raises(ExceptionGroup): test_fn() ================================================ FILE: hypothesis-python/tests/nocover/test_labels.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import strategies as st from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.strategies._internal.lazy import LazyStrategy def test_labels_are_cached(): x = st.integers() assert x.label is x.label def test_labels_are_distinct(): assert st.integers().label != st.text().label @st.composite def foo(draw): return draw(st.none()) @st.composite def bar(draw): return draw(st.none()) @st.composite def baz(draw): return draw(st.booleans()) def test_equivalent_composites_have_same_label(): assert foo().label == bar().label def test_different_composites_have_different_labels(): assert foo().label != baz().label def test_one_of_label_is_distinct(): a = st.integers() b = st.booleans() assert st.one_of(a, b).label != st.one_of(b, a).label def test_lists_label_by_element(): assert st.lists(st.integers()).label != st.lists(st.booleans()).label def test_label_of_deferred_strategy_is_well_defined(): recursive = st.deferred(lambda: st.lists(recursive)) recursive.label @pytest.mark.parametrize( "strategy", [ lambda: [st.none()], lambda: [st.integers()], lambda: [st.lists(st.floats())], lambda: [st.none(), st.integers(), st.lists(st.floats())], ], ) def test_sampled_from_label_with_strategies_does_not_change(strategy): s1 = st.sampled_from(strategy()) s2 = st.sampled_from(strategy()) assert s1.label == s2.label def test_label_of_enormous_sampled_range(): # this should not take forever. st.sampled_from(range(2**30)).label @pytest.mark.parametrize( "strategy", [ lambda: st.deferred(lambda: st.integers()), lambda: LazyStrategy(st.integers, (), {}), ], ) def test_draw_uses_wrapped_label(strategy): cd = ConjectureData.for_choices([0]) strategy = strategy() cd.draw(strategy) cd.freeze() assert len(cd.spans) == 2 assert cd.spans[1].label == st.integers().label def test_deferred_label(): strategy = st.deferred(lambda: st.integers()) assert strategy.label != st.integers().label ================================================ FILE: hypothesis-python/tests/nocover/test_large_examples.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import strategies as st from tests.common.debug import find_any def test_can_generate_large_lists_with_min_size(): find_any(st.lists(st.integers(), min_size=400)) ================================================ FILE: hypothesis-python/tests/nocover/test_limits.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, settings, strategies as st from tests.common.utils import Why, xfail_on_crosshair @xfail_on_crosshair(Why.other, strict=False) # might run fewer def test_max_examples_are_respected(): counter = 0 @given(st.random_module(), st.integers()) @settings(max_examples=100) def test(rnd, i): nonlocal counter counter += 1 test() assert counter == 100 ================================================ FILE: hypothesis-python/tests/nocover/test_modify_inner_test.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from functools import wraps import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument def always_passes(*args, **kwargs): """Stand-in for a fixed version of an inner test. For example, pytest-trio would take the inner test, wrap it in an async-to-sync converter, and use the new func (not always_passes). """ @given(st.integers()) def test_can_replace_inner_test(x): raise AssertionError("This should be replaced") test_can_replace_inner_test.hypothesis.inner_test = always_passes def decorator(func): """An example of a common decorator pattern.""" @wraps(func) def inner(*args, **kwargs): return func(*args, **kwargs) return inner @decorator @given(st.integers()) def test_can_replace_when_decorated(x): raise AssertionError("This should be replaced") test_can_replace_when_decorated.hypothesis.inner_test = always_passes @pytest.mark.parametrize("x", [1, 2]) @given(y=st.integers()) def test_can_replace_when_parametrized(x, y): raise AssertionError("This should be replaced") test_can_replace_when_parametrized.hypothesis.inner_test = always_passes def test_can_replace_when_original_is_invalid(): # Invalid: @given with too many positional arguments @given(st.integers(), st.integers()) def invalid_test(x): raise AssertionError invalid_test.hypothesis.inner_test = always_passes # Even after replacing the inner test, calling the wrapper should still # fail. with pytest.raises(InvalidArgument, match="Too many positional arguments"): invalid_test() def test_inner_is_original_even_when_invalid(): def invalid_test(x): raise AssertionError original = invalid_test # Invalid: @given with no arguments invalid_test = given()(invalid_test) # Verify that the test is actually invalid with pytest.raises( InvalidArgument, match="given must be called with at least one argument", ): invalid_test() assert invalid_test.hypothesis.inner_test == original def test_invokes_inner_function_with_args_by_name(): # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/3245 @given(st.integers()) def test(x): pass f = test.hypothesis.inner_test test.hypothesis.inner_test = wraps(f)(lambda **kw: f(**kw)) test() ================================================ FILE: hypothesis-python/tests/nocover/test_nesting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from pytest import raises from hypothesis import HealthCheck, Verbosity, given, settings, strategies as st from tests.common.utils import Why, no_shrink, xfail_on_crosshair @xfail_on_crosshair(Why.nested_given) def test_nesting_1(): @given(st.integers(0, 100)) @settings( max_examples=5, database=None, deadline=None, suppress_health_check=[HealthCheck.nested_given], ) def test_blah(x): @given(st.integers()) @settings( max_examples=100, phases=no_shrink, database=None, verbosity=Verbosity.quiet ) def test_nest(y): if y >= x: raise ValueError with raises(ValueError): test_nest() test_blah() ================================================ FILE: hypothesis-python/tests/nocover/test_precise_shrinking.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """ This file tests for our ability to make precise shrinks. Terminology: A shrink is *precise* if there is a single span (draw call) that it replaces, without leaving any of the data before or after that draw call changed. Otherwise, it is sloppy. Precise shrinks correspond to the changes we can make to drawn data in isolation of the rest of the test case. e.g. if we draw a list, we want to always be able to delete an element from it without affecting things outside that list. If we draw an integer, we always want to be able to subtract from it. An example of a sloppy shrink is that we can sloppily replace any list with a prefix of it by changing the boolean that says if we should draw more elements with False. However leaves all the data corresponding to the rest of the list after that prefix in the test case, so everything after the drawn list is deleted. Having a rich vocabulary of precise shrinks we can make allows us to more easily reason about shrunk data, because we can look at the data and think in terms of what changes the shrinker would have made to it, and the fact that it hasn't means we know that it's important. e.g. this numeric value can't be smaller, this list can't have fewer elements. Sloppy shrinks in contrast just make the test case smaller. This is still good, obviously, and we rely on sloppy shrinks for a lot of shrinker performance and quality - often what we can expect is that we get to a smaller test case faster through sloppy shrinks, and then precise shrinks guarantee properties of the final result. """ import itertools from collections.abc import Callable from functools import lru_cache from random import Random from typing import TypeVar import pytest from hypothesis import find, settings, strategies as st from hypothesis.control import BuildContext from hypothesis.errors import StopTest, UnsatisfiedAssumption from hypothesis.internal.conjecture.data import ConjectureData, ConjectureResult, Status from hypothesis.internal.conjecture.engine import ( ConjectureRunner, ExitReason, RunIsComplete, ) from hypothesis.internal.conjecture.shrinker import sort_key from tests.conjecture.common import interesting_origin T = TypeVar("T") pytestmark = [ pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="using internals for testing in a way crosshair doesn't support", ), pytest.mark.skipif( settings.get_current_profile_name() == "threading", reason="not worth making thread-safe atm", ), ] def safe_draw(data, strategy): """Set up just enough of the Hypothesis machinery to use draw on a strategy.""" with BuildContext(data, wrapped_test=None): try: return data.draw(strategy) except UnsatisfiedAssumption: data.mark_invalid() def precisely_shrink( strategy, is_interesting=lambda x: True, initial_condition=lambda x: True, end_marker=st.integers(), seed=0, ): """Generates a random value from the strategy and then precisely shrinks it, by shrinking it with some value immediately afterwards that is not allowed to be modified during shrinking.""" random = Random(seed) while True: data = ConjectureData(random=random) try: initial_value = safe_draw(data, strategy) except StopTest: continue if is_interesting(initial_value) and initial_condition(initial_value): break target_check_value = safe_draw(data, end_marker) initial_choices = data.choices replay = ConjectureData.for_choices(initial_choices) assert safe_draw(replay, strategy) == initial_value assert safe_draw(replay, end_marker) == target_check_value def test_function(data): value = safe_draw(data, strategy) check_value = safe_draw(data, end_marker) if is_interesting(value) and check_value == target_check_value: data.mark_interesting(interesting_origin()) runner = ConjectureRunner(test_function, random=random) try: buf = runner.cached_test_function(initial_choices) assert buf.status == Status.INTERESTING assert buf.choices == initial_choices assert runner.interesting_examples runner.shrink_interesting_examples() except RunIsComplete: assert runner.exit_reason in (ExitReason.finished, ExitReason.max_shrinks) (result,) = runner.interesting_examples.values() data = ConjectureData.for_choices(result.choices) result_value = safe_draw(data, strategy) data.freeze() return data.as_result(), result_value common_strategies_with_types = [ (type(None), st.none()), (bool, st.booleans()), (bytes, st.binary()), (str, st.text()), (int, st.integers()), ] common_strategies = [v for _, v in common_strategies_with_types] @lru_cache def minimal_for_strategy(s): return precisely_shrink(s, end_marker=st.none()) def minimal_nodes_for_strategy(s): return minimal_for_strategy(s)[0].nodes def test_strategy_list_is_in_sorted_order(): assert common_strategies == sorted( common_strategies, key=lambda s: sort_key(minimal_nodes_for_strategy(s)) ) @pytest.mark.parametrize("typ,strat", common_strategies_with_types) @pytest.mark.parametrize("require_truthy", [False, True]) def test_can_precisely_shrink_values(typ, strat, require_truthy): if typ is type(None) and require_truthy: pytest.skip("None is falsey") if require_truthy: cond = bool else: cond = lambda x: True _result, shrunk = precisely_shrink(strat, is_interesting=cond) assert shrunk == find(strat, cond) alternatives = [ comb for n in (2, 3, 4) for comb in itertools.combinations(common_strategies_with_types, n) ] indexed_alternatives = [ (i, j, a) for a in alternatives for i, j in itertools.combinations(range(len(a)), 2) ] @pytest.mark.parametrize("i,j,a", indexed_alternatives) @pytest.mark.parametrize("seed", [0, 4389048901]) def test_can_precisely_shrink_alternatives(i, j, a, seed): types = [u for u, _ in a] combined_strategy = st.one_of(*[v for _, v in a]) _result, value = precisely_shrink( combined_strategy, initial_condition=lambda x: isinstance(x, types[j]), is_interesting=lambda x: not any(isinstance(x, types[k]) for k in range(i)), seed=seed, ) assert isinstance(value, types[i]) @pytest.mark.parametrize( "a", list(itertools.combinations(common_strategies_with_types, 3)) ) @pytest.mark.parametrize("seed", [0, 4389048901]) def test_precise_shrink_with_blocker(a, seed): # We're reordering this so that there is a "blocking" unusually large # strategy in the middle. x, y, z = a a = (x, z, y) types = [u for u, _ in a] combined_strategy = st.one_of(*[v for _, v in a]) _result, value = precisely_shrink( combined_strategy, initial_condition=lambda x: isinstance(x, types[2]), is_interesting=lambda x: True, seed=seed, ) assert isinstance(value, types[0]) def find_random( s: st.SearchStrategy[T], condition: Callable[[T], bool], seed=None ) -> tuple[ConjectureResult, T]: random = Random(seed) while True: data = ConjectureData(random=random) try: with BuildContext(data=data, wrapped_test=None): value = data.draw(s) if condition(value): data.freeze() return (data.as_result(), value) except (StopTest, UnsatisfiedAssumption): continue def shrinks(strategy, nodes, *, allow_sloppy=True, seed=0): results = {} random = Random(seed) choices = tuple(n.value for n in nodes) if allow_sloppy: def test_function(data): value = safe_draw(data, strategy) results[data.nodes] = value runner = ConjectureRunner(test_function, settings=settings(max_examples=10**9)) initial = runner.cached_test_function(choices) assert isinstance(initial, ConjectureResult) try: runner.shrink(initial, lambda x: x.choices == initial.choices) except RunIsComplete: assert runner.exit_reason in (ExitReason.finished, ExitReason.max_shrinks) else: trial = ConjectureData(prefix=choices, random=random) with BuildContext(trial, wrapped_test=None): trial.draw(strategy) assert trial.choices == choices, "choice sequence is already sloppy" padding = safe_draw(trial, st.integers()) initial_choices = trial.choices def test_function(data): value = safe_draw(data, strategy) key = data.nodes padding_check = safe_draw(data, st.integers()) if padding_check == padding: results[key] = value runner = ConjectureRunner(test_function, settings=settings(max_examples=10**9)) initial = runner.cached_test_function(initial_choices) assert len(results) == 1 try: runner.shrink(initial, lambda x: x.choices == initial_choices) except RunIsComplete: assert runner.exit_reason in (ExitReason.finished, ExitReason.max_shrinks) results.pop(nodes) seen = set() result_list = [] for k, v in sorted(results.items(), key=lambda x: sort_key(x[0])): t = repr(v) if t in seen: continue seen.add(t) result_list.append((k, v)) return result_list @pytest.mark.parametrize("a", list(itertools.product(*([common_strategies[1:]] * 2)))) @pytest.mark.parametrize("block_falsey", [False, True]) @pytest.mark.parametrize("allow_sloppy", [False, True]) @pytest.mark.parametrize("seed", [0, 2452, 99085240570]) def test_always_shrinks_to_none(a, seed, block_falsey, allow_sloppy): combined_strategy = st.one_of(st.none(), *a) result, _value = find_random(combined_strategy, lambda x: x is not None) shrunk_values = shrinks( combined_strategy, result.nodes, allow_sloppy=allow_sloppy, seed=seed ) assert shrunk_values[0][1] is None @pytest.mark.parametrize( "i,alts", [(i, alt) for alt in alternatives for i in range(1, len(alt))] ) @pytest.mark.parametrize("force_small", [False, True]) @pytest.mark.parametrize("seed", [0, 2452, 99085240570]) def test_can_shrink_to_every_smaller_alternative(i, alts, seed, force_small): types = [t for t, _ in alts] strats = [s for _, s in alts] combined_strategy = st.one_of(*strats) if force_small: result, _value = precisely_shrink( combined_strategy, is_interesting=lambda x: type(x) is types[i], seed=seed ) else: result, _value = find_random( combined_strategy, lambda x: type(x) is types[i], seed=seed ) shrunk = shrinks( combined_strategy, result.nodes, allow_sloppy=False, # Arbitrary change so we don't use the same seed for each Random. seed=seed * 17, ) shrunk_values = [t for _, t in shrunk] for j in range(i): assert any(isinstance(x, types[j]) for x in shrunk_values) ================================================ FILE: hypothesis-python/tests/nocover/test_pretty_repr.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import OrderedDict import pytest from hypothesis import given, strategies as st from hypothesis.control import reject from hypothesis.errors import HypothesisDeprecationWarning, InvalidArgument def foo(x): pass def bar(x): pass def baz(x): pass fns = [foo, bar, baz] def builds_ignoring_invalid(target, *args, **kwargs): def splat(value): try: result = target(*value[0], **value[1]) result.validate() return result except (HypothesisDeprecationWarning, InvalidArgument): reject() return st.tuples(st.tuples(*args), st.fixed_dictionaries(kwargs)).map(splat) size_strategies = { "min_size": st.integers(min_value=0, max_value=100), "max_size": st.integers(min_value=0, max_value=100) | st.none(), } values = st.integers() | st.text() Strategies = st.recursive( st.one_of( st.sampled_from( [ st.none(), st.booleans(), st.randoms(use_true_random=True), st.complex_numbers(), st.randoms(use_true_random=True), st.fractions(), st.decimals(), ] ), st.builds(st.just, values), st.builds(st.sampled_from, st.lists(values, min_size=1)), builds_ignoring_invalid(st.floats, st.floats(), st.floats()), ), lambda x: st.one_of( builds_ignoring_invalid(st.lists, x, **size_strategies), builds_ignoring_invalid(st.sets, x, **size_strategies), builds_ignoring_invalid(lambda v: st.tuples(*v), st.lists(x)), builds_ignoring_invalid(lambda v: st.one_of(*v), st.lists(x, min_size=1)), builds_ignoring_invalid( st.dictionaries, x, x, dict_class=st.sampled_from([dict, OrderedDict]), **size_strategies, ), st.builds(lambda s, f: s.map(f), x, st.sampled_from(fns)), ), ) strategy_globals = {k: getattr(st, k) for k in dir(st)} strategy_globals["OrderedDict"] = OrderedDict strategy_globals["inf"] = float("inf") strategy_globals["nan"] = float("nan") strategy_globals["foo"] = foo strategy_globals["bar"] = bar strategy_globals["baz"] = baz @given(Strategies) def test_repr_evals_to_thing_with_same_repr(strategy): r = repr(strategy) via_eval = eval(r, strategy_globals) r2 = repr(via_eval) assert r == r2 @pytest.mark.parametrize( "r", [ "none().filter(foo).map(bar)", "just(1).filter(foo).map(bar)", "sampled_from([1, 2, 3]).filter(foo).map(bar)", ], ) def test_sampled_transform_reprs(r): assert repr(eval(r, strategy_globals)) == r ================================================ FILE: hypothesis-python/tests/nocover/test_randomization.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from random import Random import pytest from hypothesis import ( HealthCheck, Verbosity, core, given, settings, strategies as st, ) from tests.common.utils import Why, no_shrink, xfail_on_crosshair @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="we do not yet pass backends the global random seed, so they are not deterministic", ) def test_seeds_off_internal_random(): choices1 = [] choices2 = [] @given(st.integers()) def f1(n): choices1.append(n) @given(st.integers()) def f2(n): choices2.append(n) core.threadlocal._hypothesis_global_random = Random(0) state = core.threadlocal._hypothesis_global_random.getstate() f1() core.threadlocal._hypothesis_global_random.setstate(state) f2() assert choices1 == choices2 @xfail_on_crosshair(Why.nested_given) def test_nesting_with_control_passes_health_check(): @given(st.integers(0, 100), st.random_module()) @settings( max_examples=5, database=None, deadline=None, suppress_health_check=[HealthCheck.nested_given], ) def test_blah(x, rnd): @given(st.integers()) @settings( max_examples=100, phases=no_shrink, database=None, verbosity=Verbosity.quiet ) def test_nest(y): assert y < x with pytest.raises(AssertionError): test_nest() test_blah() ================================================ FILE: hypothesis-python/tests/nocover/test_recursive.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys import threading import pytest from hypothesis import HealthCheck, given, settings, strategies as st from tests.common.debug import find_any, minimal from tests.common.utils import Why, flaky, xfail_if_gil_disabled, xfail_on_crosshair def test_can_generate_with_large_branching(): def flatten(x): if isinstance(x, list): return sum(map(flatten, x), []) else: return [x] size = 20 xs = minimal( st.recursive( st.integers(), lambda x: st.lists(x, min_size=size // 2), max_leaves=size * 2, ), lambda x: isinstance(x, list) and len(flatten(x)) >= size, ) assert flatten(xs) == [0] * size def test_can_generate_some_depth_with_large_branching(): def depth(x): if x and isinstance(x, list): return 1 + max(map(depth, x)) else: return 1 xs = minimal(st.recursive(st.integers(), st.lists), lambda x: depth(x) > 1) assert xs in ([0], [[]]) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes 2 hours" ) def test_can_find_quite_broad_lists(): def breadth(x): if isinstance(x, list): return sum(map(breadth, x)) else: return 1 target = 10 broad = minimal( st.recursive(st.booleans(), lambda x: st.lists(x, max_size=target // 2)), lambda x: breadth(x) >= target, settings=settings(max_examples=10000), ) assert breadth(broad) == target def test_drawing_many_near_boundary(): size = 4 elems = st.recursive( st.booleans(), lambda x: st.lists(x, min_size=2 * (size - 1), max_size=2 * size).map(tuple), max_leaves=2 * size - 1, ) ls = minimal(st.lists(elems), lambda x: len(set(x)) >= size) assert len(ls) == size @xfail_on_crosshair(Why.undiscovered) def test_can_use_recursive_data_in_sets(): nested_sets = st.recursive(st.booleans(), st.frozensets, max_leaves=3) find_any(nested_sets, settings=settings(deadline=None)) def flatten(x): if isinstance(x, bool): return frozenset((x,)) else: result = frozenset() for t in x: result |= flatten(t) if len(result) == 2: break return result x = minimal(nested_sets, lambda x: len(flatten(x)) == 2, settings(deadline=None)) assert x in ( frozenset((False, True)), frozenset((False, frozenset((True,)))), frozenset((frozenset((False, True)),)), ) @flaky(max_runs=2, min_passes=1) def test_can_form_sets_of_recursive_data(): size = 3 trees = st.sets( st.recursive( st.booleans(), lambda x: st.lists(x, min_size=size).map(tuple), max_leaves=20, ) ) xs = minimal(trees, lambda x: len(x) >= size) assert len(xs) == size @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="not threadsafe" ) def test_drawing_from_recursive_strategy_is_thread_safe(): shared_strategy = st.recursive( st.integers(), lambda s: st.lists(s, max_size=2), max_leaves=20 ) errors = [] @settings( database=None, deadline=None, suppress_health_check=[HealthCheck.too_slow] ) @given(data=st.data()) def test(data): try: data.draw(shared_strategy) except Exception as exc: errors.append(exc) original_recursionlimit = sys.getrecursionlimit() threads = [] for _ in range(4): threads.append(threading.Thread(target=test)) for thread in threads: thread.start() for thread in threads: thread.join() assert sys.getrecursionlimit() == original_recursionlimit assert not errors SELF_REF = st.recursive( st.deferred(lambda: st.booleans() | SELF_REF), lambda s: st.lists(s, min_size=1), ) @settings( suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], max_examples=5, ) @given(SELF_REF) def test_self_ref_regression(_): # See https://github.com/HypothesisWorks/hypothesis/issues/2794 pass @pytest.mark.skipif(sys.version_info >= (3, 14), reason="gc changes") @xfail_if_gil_disabled @flaky(min_passes=1, max_runs=2) def test_gc_hooks_do_not_cause_unraisable_recursionerror(testdir): # We were concerned in #3979 that we might see bad results from a RecursionError # inside the GC hook, if the stack was already deep and someone (e.g. Pytest) # had installed a sys.unraisablehook which raises that later. # This test is potentially flaky, because the stack usage of a function is not # constant. Regardless, if the test passes just once that's sufficient proof that # it's not the GC (or accounting of it) that is at fault. Note, I haven't actually # seen it fail/flake, but I believe it could happen in principle. # # What we *have* seen on CI with xdist is flaky segmentation faults. Hence, the # test is executed in a subprocess. script = """ import gc import pytest from hypothesis import given, strategies as st # The number of cycles sufficient to reliably trigger GC, experimentally found # to be a few hundred on CPython. Multiply by 10 for safety margin. NUM_CYCLES = 5_000 def probe_depth(): try: return probe_depth() + 1 except RecursionError: return 0 def at_depth(depth, fn): if depth <= 1: return fn() else: # Recurse towards requested depth return at_depth(depth - 1, fn) def gen_cycles(): for _ in range(NUM_CYCLES): a = [None] b = [a] a[0] = b def gen_cycles_at_depth(depth, *, gc_disable): try: if gc_disable: gc.disable() at_depth(depth, gen_cycles) dead_objects = gc.collect() if dead_objects is not None: # is None on PyPy if gc_disable: assert dead_objects >= 2 * NUM_CYCLES else: # collection was triggered assert dead_objects < 2 * NUM_CYCLES finally: gc.enable() # Warmup to de-flake PyPy (the first run has much lower effective limits) probe_depth() @given(st.booleans()) def test_gc_hooks_recursive(_): max_depth = probe_depth() # Lower the limit to where we can successfully generate cycles # when no gc is performed while True: try: gen_cycles_at_depth(max_depth, gc_disable=True) except RecursionError: max_depth -= 1 else: break # Note that PyPy is a bit weird, in that it raises RecursionError at # (maxdepth - n) for small positive n, but not at exactly (maxdepth). # In general, it is really finicky to get the details right in this # test, so be careful. # Now check that the limit is unchanged with gc enabled, and also that # leaving a few frames for the callbacks does not fail. if hasattr(gc, "callbacks"): # see comment above for n in range(1, 4): gen_cycles_at_depth(max_depth - n, gc_disable=False) gen_cycles_at_depth(max_depth, gc_disable=False) with pytest.raises(RecursionError): gen_cycles_at_depth(max_depth + 1, gc_disable=False) """ testdir.makepyfile(script) testdir.runpytest_subprocess().assert_outcomes(passed=1) ================================================ FILE: hypothesis-python/tests/nocover/test_regex.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import string import sys from functools import reduce import pytest from hypothesis import assume, given, reject, strategies as st from hypothesis.strategies._internal.regex import ( IncompatibleWithAlphabet, base_regex_strategy, ) @st.composite def charset(draw): negated = draw(st.booleans()) chars = draw(st.text(string.ascii_letters + string.digits, min_size=1)) if negated: return f"[^{chars}]" else: return f"[{chars}]" COMBINED_MATCHER = re.compile("[?+*]{2}") @st.composite def conservative_regex(draw): result = draw( st.one_of( st.just("."), st.sampled_from([re.escape(c) for c in string.printable]), charset(), CONSERVATIVE_REGEX.map(lambda s: f"({s})"), CONSERVATIVE_REGEX.map(lambda s: s + "+"), CONSERVATIVE_REGEX.map(lambda s: s + "?"), CONSERVATIVE_REGEX.map(lambda s: s + "*"), st.lists(CONSERVATIVE_REGEX, min_size=1, max_size=3).map("|".join), st.lists(CONSERVATIVE_REGEX, min_size=1, max_size=3).map("".join), ) ) assume(COMBINED_MATCHER.search(result) is None) control = sum(result.count(c) for c in "?+*") assume(control <= 3) assume(I_WITH_DOT not in result) # known to be weird return result CONSERVATIVE_REGEX = conservative_regex() FLAGS = st.sets( st.sampled_from([re.ASCII, re.IGNORECASE, re.MULTILINE, re.DOTALL]) ).map(lambda flag_set: reduce(int.__or__, flag_set, 0)) @given(st.data()) def test_conservative_regex_are_correct_by_construction(data): pattern = re.compile(data.draw(CONSERVATIVE_REGEX), flags=data.draw(FLAGS)) result = data.draw(base_regex_strategy(pattern, alphabet=st.characters())) # We'll skip "capital I with dot above" due to awful casefolding behaviour # and "latin small letter dotless i" for the same reason. assume({"ı", "İ"}.isdisjoint(pattern.pattern + result)) assert pattern.search(result) is not None @given(st.data()) def test_fuzz_stuff(data): pattern = data.draw( st.text(min_size=1, max_size=5) | st.binary(min_size=1, max_size=5) | CONSERVATIVE_REGEX.filter(bool) ) flags = data.draw(FLAGS) try: regex = re.compile(pattern, flags=flags) except (re.error, FutureWarning): # Possible nested sets, e.g. "[[", trigger a FutureWarning reject() try: ex = data.draw(st.from_regex(regex)) except IncompatibleWithAlphabet: if isinstance(pattern, str) and flags & re.ASCII: with pytest.raises(UnicodeEncodeError): pattern.encode("ascii") regex = re.compile(pattern, flags=flags ^ re.ASCII) ex = data.draw(st.from_regex(regex)) else: raise assert regex.search(ex) @pytest.mark.skipif(sys.version_info[:2] < (3, 11), reason="new syntax") @given(st.data()) def test_regex_atomic_group(data): pattern = "a(?>bc|b)c" ex = data.draw(st.from_regex(pattern)) assert re.search(pattern, ex) @pytest.mark.skipif(sys.version_info[:2] < (3, 11), reason="new syntax") @given(st.data()) def test_regex_possessive(data): pattern = '"[^"]*+"' ex = data.draw(st.from_regex(pattern)) assert re.search(pattern, ex) # Some preliminaries, to establish what's happening: I_WITH_DOT = "\u0130" assert I_WITH_DOT.swapcase() == "i\u0307" # note: string of length two! assert re.compile(I_WITH_DOT, flags=re.IGNORECASE).match(I_WITH_DOT.swapcase()) @given(st.data()) def test_case_insensitive_not_literal_never_constructs_multichar_match(data): # So our goal is to confirm that we can never accidentally create a non-matching # string by assembling individually allowed characters. pattern = re.compile(f"[^{I_WITH_DOT}]+", flags=re.IGNORECASE) strategy = st.from_regex(pattern, fullmatch=True) for _ in range(5): s = data.draw(strategy) assert pattern.fullmatch(s) is not None # And to be on the safe side, we implement this stronger property: assert set(s).isdisjoint(I_WITH_DOT.swapcase()) @given(st.from_regex(re.compile(f"[^{I_WITH_DOT}_]", re.IGNORECASE), fullmatch=True)) def test_no_error_converting_negated_sets_to_strategy(s): # CharactersBuilder no longer triggers an internal error converting sets # or negated sets to a strategy when multi-char strings are whitelisted. pass ================================================ FILE: hypothesis-python/tests/nocover/test_regressions.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, settings, strategies as st from hypothesis._settings import note_deprecation from hypothesis.errors import HypothesisDeprecationWarning from tests.common.utils import Why, xfail_on_crosshair def test_note_deprecation_blames_right_code_issue_652(): msg = "this is an arbitrary deprecation warning message" @st.composite def deprecated_strategy(draw): draw(st.none()) note_deprecation(msg, since="RELEASEDAY", has_codemod=False) @given(deprecated_strategy()) def f(x): pass with pytest.warns(HypothesisDeprecationWarning) as log: f() assert len(log) == 1 (record,) = log # We got the warning we expected, from the right file assert isinstance(record.message, HypothesisDeprecationWarning) assert record.message.args == (msg,) assert record.filename == __file__ @given( x=st.one_of(st.just(0) | st.just(1)), y=st.one_of(st.just(0) | st.just(1) | st.just(2)), ) def test_performance_issue_2027(x, y): pass @given( st.lists( st.floats(allow_infinity=False), unique=True, ) ) def test_unique_floats_with_nan_is_not_flaky_3926(ls): pass # this will take a while to find the regression, but will eventually trigger it. # min_value=0 is critical to trigger the probing behavior which exhausts our buffer. # https://github.com/pschanely/CrossHair/issues/285 for an upstream fix. @xfail_on_crosshair(Why.other, strict=False) @given(st.integers(min_value=0, max_value=1 << 25_000)) def test_overrun_during_datatree_simulation_3874(n): pass def test_explain_phase_label_assertion_4339(): # st.composite causes a re-creation of the SampledFromStrategy each time # (one_of is implemented using sampled_from internally), which previously # had different labels which triggered an assertion in the explain code. @st.composite def g(draw): draw(st.none() | st.booleans()) @given(g(), st.none() | st.booleans()) @settings(database=None) def f(a, b): raise ValueError("marker") with pytest.raises(ValueError, match="marker"): f() ================================================ FILE: hypothesis-python/tests/nocover/test_reusable_values.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import example, given, strategies as st from hypothesis.errors import InvalidArgument # Be aware that tests in this file pass strategies as arguments to @example. # That's normally a mistake, but for these tests it's intended. # If one of these tests fails, Hypothesis will complain about the # @example/strategy interaction, but it should be safe to ignore that # error message and focus on the underlying failure instead. base_reusable_strategies = ( st.text(), st.binary(), st.dates(), st.times(), st.timedeltas(), st.booleans(), st.complex_numbers(), st.floats(), st.floats(-1.0, 1.0), st.integers(), st.integers(1, 10), st.integers(1), # Note that `just` and `sampled_from` count as "reusable" even if their # values are mutable, because the user has implicitly promised that they # don't care about the same mutable value being returned by separate draws. st.just([]), st.sampled_from([[]]), st.tuples(st.integers()), ) @st.deferred def reusable(): """Meta-strategy that produces strategies that should have ``.has_reusable_values == True``.""" return st.one_of( # This looks like it should be `one_of`, but `sampled_from` is correct # because we want this meta-strategy to yield strategies as its values. st.sampled_from(base_reusable_strategies), # This sometimes produces invalid combinations of arguments, which # we filter out further down with an explicit validation check. st.builds( st.floats, min_value=st.none() | st.floats(allow_nan=False), max_value=st.none() | st.floats(allow_nan=False), allow_infinity=st.booleans(), allow_nan=st.booleans(), ), st.builds(st.just, st.builds(list)), st.builds(st.sampled_from, st.lists(st.builds(list), min_size=1)), st.lists(reusable).map(st.one_of), st.lists(reusable).map(lambda ls: st.tuples(*ls)), ) def is_valid(s): try: s.validate() return True except InvalidArgument: return False reusable = reusable.filter(is_valid) assert not reusable.is_empty def many_examples(examples): """Helper decorator to apply the ``@example`` decorator multiple times, once for each given example.""" def accept(f): for e in examples: f = example(e)(f) return f return accept @many_examples(base_reusable_strategies) @many_examples(st.tuples(s) for s in base_reusable_strategies) @given(reusable) def test_reusable_strategies_are_all_reusable(s): assert s.has_reusable_values @many_examples(base_reusable_strategies) @given(reusable) def test_filter_breaks_reusability(s): cond = True def nontrivial_filter(x): """Non-trivial filtering function, intended to remain opaque even if some strategies introspect their filters.""" return cond assert s.has_reusable_values assert not s.filter(nontrivial_filter).has_reusable_values @many_examples(base_reusable_strategies) @given(reusable) def test_map_breaks_reusability(s): cond = True def nontrivial_map(x): """Non-trivial mapping function, intended to remain opaque even if some strategies introspect their mappings.""" if cond: return x else: return None assert s.has_reusable_values assert not s.map(nontrivial_map).has_reusable_values @many_examples(base_reusable_strategies) @given(reusable) def test_flatmap_breaks_reusability(s): cond = True def nontrivial_flatmap(x): """Non-trivial flat-mapping function, intended to remain opaque even if some strategies introspect their flat-mappings.""" if cond: return st.just(x) else: return st.none() assert s.has_reusable_values assert not s.flatmap(nontrivial_flatmap).has_reusable_values @pytest.mark.parametrize( "strat", [ st.lists(st.booleans()), st.sets(st.booleans()), st.dictionaries(st.booleans(), st.booleans()), ], ) def test_mutable_collections_do_not_have_reusable_values(strat): assert not strat.has_reusable_values def test_recursion_does_not_break_reusability(): x = st.deferred(lambda: st.none() | st.tuples(x)) assert x.has_reusable_values ================================================ FILE: hypothesis-python/tests/nocover/test_sampled_from.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import enum import functools import itertools import operator import pytest from hypothesis import given, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.internal.compat import bit_count from hypothesis.strategies._internal.strategies import SampledFromStrategy from tests.common.debug import find_any, minimal from tests.common.utils import fails_with @pytest.mark.parametrize("size", [100, 10**5, 10**6, 2**25]) @given(data=st.data()) def test_filter_large_lists(data, size): n_calls = 0 def cond(x): nonlocal n_calls n_calls += 1 return x % 2 != 0 s = data.draw(st.sampled_from(range(size)).filter(cond)) assert s % 2 != 0 assert n_calls <= SampledFromStrategy._MAX_FILTER_CALLS def rare_value_strategy(n, target): def forbid(s, forbidden): """Helper function to avoid Python variable scoping issues.""" return s.filter(lambda x: x != forbidden) s = st.sampled_from(range(n)) for i in range(n): if i != target: s = forbid(s, i) return s @given(rare_value_strategy(n=128, target=80)) def test_chained_filters_find_rare_value(x): assert x == 80 @fails_with(InvalidArgument) @given(st.sets(st.sampled_from(range(10)), min_size=11)) def test_unsat_sets_of_samples(x): raise AssertionError @given(st.sets(st.sampled_from(range(50)), min_size=50)) def test_efficient_sets_of_samples(x): assert x == set(range(50)) class AnEnum(enum.Enum): a = enum.auto() b = enum.auto() def test_enum_repr_uses_class_not_a_list(): # The repr should have enough detail to find the class again # (which is very useful for the ghostwriter logic we're working on) lazy_repr = repr(st.sampled_from(AnEnum)) assert lazy_repr == "sampled_from(tests.nocover.test_sampled_from.AnEnum)" def test_repr_truncates_with_many_elements(): s = st.sampled_from(list(range(10_000))) repr_limit = 512 assert repr(s) == f"sampled_from([{', '.join(map(str, range(repr_limit)))}, ...])" class AFlag(enum.Flag): a = enum.auto() b = enum.auto() c = enum.auto() LargeFlag = enum.Flag("LargeFlag", {f"bit{i}": enum.auto() for i in range(64)}) class UnnamedFlag(enum.Flag): # Would fail under EnumCheck.NAMED_FLAGS a = 0 b = 7 def test_flag_enum_repr_uses_class_not_a_list(): lazy_repr = repr(st.sampled_from(AFlag)) assert lazy_repr == "sampled_from(tests.nocover.test_sampled_from.AFlag)" def test_exhaustive_flags(): # Generate powerset of flag combinations. There are only 2^3 of them, so # we can reasonably expect that they are all are found. unseen_flags = { functools.reduce(operator.or_, flaglist, AFlag(0)) for r in range(len(AFlag) + 1) for flaglist in itertools.combinations(AFlag, r) } @given(st.sampled_from(AFlag)) def accept(flag): unseen_flags.discard(flag) accept() assert not unseen_flags def test_flags_minimize_to_first_named_flag(): assert minimal(st.sampled_from(LargeFlag)) == LargeFlag.bit0 def test_flags_minimizes_bit_count(): assert ( minimal(st.sampled_from(LargeFlag), lambda f: bit_count(f.value) > 1) == LargeFlag.bit0 | LargeFlag.bit1 ) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~10 mins; path tree is too large", ) def test_flags_finds_all_bits_set(): assert find_any(st.sampled_from(LargeFlag), lambda f: f == ~LargeFlag(0)) def test_sample_unnamed_alias(): assert find_any(st.sampled_from(UnnamedFlag), lambda f: f == UnnamedFlag.b) def test_shrink_to_named_empty(): assert minimal(st.sampled_from(UnnamedFlag)) == UnnamedFlag(0) ================================================ FILE: hypothesis-python/tests/nocover/test_scrutineer.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import json import sys import sysconfig import pytest from hypothesis import given, note, settings, strategies as st from hypothesis.internal.compat import PYPY from hypothesis.internal.scrutineer import make_report from hypothesis.vendor import pretty from tests.common.utils import skipif_threading # We skip tracing for explanations under PyPy, where it has a large performance # impact, or if there is already a trace function (e.g. coverage or a debugger) pytestmark = pytest.mark.skipif(PYPY or sys.gettrace(), reason="See comment") BUG_MARKER = "# BUG" DEADLINE_PRELUDE = """ from datetime import timedelta from hypothesis.errors import DeadlineExceeded """ PRELUDE = """ from hypothesis import Phase, given, settings, strategies as st @settings(phases=tuple(Phase), derandomize=True) """ TRIVIAL = """ @given(st.integers()) def test_reports_branch_in_test(x): if x > 10: raise AssertionError # BUG """ MULTIPLE_BUGS = """ @given(st.integers(), st.integers()) def test_reports_branch_in_test(x, y): if x > 10: raise (AssertionError if x % 2 else Exception) # BUG """ FRAGMENTS = ( pytest.param(TRIVIAL, id="trivial"), pytest.param(MULTIPLE_BUGS, id="multiple-bugs"), ) def get_reports(file_contents, *, testdir): # Takes the source code string with "# BUG" comments, and returns a list of # multi-line report strings which we expect to see in explain-mode output. # The list length is the number of explainable bugs, usually one. test_file = str(testdir.makepyfile(file_contents)) pytest_stdout = str(testdir.runpytest_inprocess(test_file, "--tb=native").stdout) crash = "AttributeError: module 'blib2to3.pygram' has no attribute 'python_symbols'" if crash in pytest_stdout: pytest.xfail(reason="upstream error in Black") explanations = { i: {(test_file, i)} for i, line in enumerate(file_contents.splitlines()) if line.endswith(BUG_MARKER) } expected = [ ("\n".join(r), "\n | ".join(r)) # single, ExceptionGroup for r in make_report(explanations).values() ] return pytest_stdout, expected @skipif_threading # runpytest_inprocess is not thread safe @pytest.mark.parametrize("code", FRAGMENTS) def test_explanations(code, testdir): pytest_stdout, expected = get_reports(PRELUDE + code, testdir=testdir) assert len(expected) == code.count(BUG_MARKER) for single, group in expected: assert single in pytest_stdout or group in pytest_stdout @skipif_threading # runpytest_inprocess is not thread safe @pytest.mark.parametrize("code", FRAGMENTS) def test_no_explanations_if_deadline_exceeded(code, testdir): code = code.replace("AssertionError", "DeadlineExceeded(timedelta(), timedelta())") pytest_stdout, _ = get_reports(DEADLINE_PRELUDE + PRELUDE + code, testdir=testdir) assert "Explanation:" not in pytest_stdout NO_SHOW_CONTEXTLIB = """ from contextlib import contextmanager from hypothesis import given, strategies as st, Phase, settings @contextmanager def ctx(): yield @settings(phases=list(Phase)) @given(st.integers()) def test(x): with ctx(): assert x < 100 """ @skipif_threading # runpytest_inprocess is not thread safe @pytest.mark.skipif(PYPY, reason="Tracing is slow under PyPy") def test_skips_uninformative_locations(testdir): pytest_stdout, _ = get_reports(NO_SHOW_CONTEXTLIB, testdir=testdir) assert "Explanation:" not in pytest_stdout @given(st.randoms()) @settings(max_examples=5) def test_report_sort(random): # show local files first, then site-packages, then stdlib lines = [ # local (__file__, 10), # site-packages (pytest.__file__, 123), (pytest.__file__, 124), # stdlib (json.__file__, 43), (json.__file__, 42), ] random.shuffle(lines) explanations = {"origin": lines} report = make_report(explanations) report_lines = report["origin"][2:] report_lines = [line.strip() for line in report_lines] expected_lines = [ f"{__file__}:10", f"{pytest.__file__}:123", f"{pytest.__file__}:124", f"{json.__file__}:42", f"{json.__file__}:43", ] note(f"sysconfig.get_paths(): {pretty.pretty(sysconfig.get_paths())}") note(f"actual lines: {pretty.pretty(report_lines)}") note(f"expected lines: {pretty.pretty(expected_lines)}") assert report_lines == expected_lines ================================================ FILE: hypothesis-python/tests/nocover/test_sets.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, settings from hypothesis.strategies import floats, integers, sets from tests.common.debug import find_any def test_can_draw_sets_of_hard_to_find_elements(): rarebool = floats(0, 1).map(lambda x: x <= 0.05) find_any(sets(rarebool, min_size=2), settings=settings(deadline=None)) @given(sets(integers(), max_size=0)) def test_empty_sets(x): assert x == set() @given(sets(integers(), max_size=2)) def test_bounded_size_sets(x): assert len(x) <= 2 ================================================ FILE: hypothesis-python/tests/nocover/test_sharing.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st from tests.common.debug import find_any, minimal x = st.shared(st.integers()) @given(x, x) def test_sharing_is_by_instance_by_default(a, b): assert a == b @given(st.shared(st.integers(), key="hi"), st.shared(st.integers(), key="hi")) def test_different_instances_with_the_same_key_are_shared(a, b): assert a == b def test_different_instances_are_not_shared(): find_any( st.tuples(st.shared(st.integers()), st.shared(st.integers())), lambda x: x[0] != x[1], ) def test_different_keys_are_not_shared(): find_any( st.tuples(st.shared(st.integers(), key=1), st.shared(st.integers(), key=2)), lambda x: x[0] != x[1], ) def test_keys_and_default_are_not_shared(): find_any( st.tuples(st.shared(st.integers(), key=1), st.shared(st.integers())), lambda x: x[0] != x[1], ) def test_can_simplify_shared_lists(): xs = minimal( st.lists(st.shared(st.integers())), lambda x: len(x) >= 10 and x[0] != 0 ) assert xs == [1] * 10 def test_simplify_shared_linked_to_size(): xs = minimal(st.lists(st.shared(st.integers())), lambda t: sum(t) >= 1000) assert sum(xs[:-1]) < 1000 assert (xs[0] - 1) * len(xs) < 1000 ================================================ FILE: hypothesis-python/tests/nocover/test_simple_numbers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import sys import pytest from hypothesis import given from hypothesis.strategies import floats, integers, lists from tests.common.debug import minimal from tests.common.utils import Why, xfail_on_crosshair def test_minimize_negative_int(): assert minimal(integers(), lambda x: x < 0) == -1 assert minimal(integers(), lambda x: x < -1) == -2 def test_positive_negative_int(): assert minimal(integers(), lambda x: x > 0) == 1 assert minimal(integers(), lambda x: x > 1) == 2 boundaries = pytest.mark.parametrize( "boundary", sorted( [2**i for i in range(10)] + [2**i - 1 for i in range(10)] + [2**i + 1 for i in range(10)] + [10**i for i in range(6)] ), ) @boundaries def test_minimizes_int_down_to_boundary(boundary): assert minimal(integers(), lambda x: x >= boundary) == boundary @boundaries def test_minimizes_int_up_to_boundary(boundary): assert minimal(integers(), lambda x: x <= -boundary) == -boundary @boundaries def test_minimizes_ints_from_down_to_boundary(boundary): def is_good(x): assert x >= boundary - 10 return x >= boundary assert minimal(integers(min_value=boundary - 10), is_good) == boundary assert minimal(integers(min_value=boundary)) == boundary def test_minimizes_negative_integer_range_upwards(): assert minimal(integers(min_value=-10, max_value=-1)) == -1 @boundaries def test_minimizes_integer_range_to_boundary(boundary): assert minimal(integers(boundary, boundary + 100)) == boundary def test_single_integer_range_is_range(): assert minimal(integers(1, 1)) == 1 def test_minimal_small_number_in_large_range(): assert minimal(integers((-(2**32)), 2**32), lambda x: x >= 101) == 101 def test_minimal_small_sum_float_list(): xs = minimal(lists(floats(), min_size=5), lambda x: sum(x) >= 1.0) assert xs == [0.0, 0.0, 0.0, 0.0, 1.0] def test_minimals_boundary_floats(): def f(x): print(x) return True assert minimal(floats(min_value=-1, max_value=1), f) == 0 def test_minimal_non_boundary_float(): x = minimal(floats(min_value=1, max_value=9), lambda x: x > 2) assert x == 3 # (the smallest integer > 2) def test_minimal_float_is_zero(): assert minimal(floats()) == 0.0 def test_minimal_asymetric_bounded_float(): assert minimal(floats(min_value=1.1, max_value=1.6)) == 1.5 def test_negative_floats_simplify_to_zero(): assert minimal(floats(), lambda x: x <= -1.0) == -1.0 def test_minimal_infinite_float_is_positive(): assert minimal(floats(), math.isinf) == math.inf def test_can_minimal_infinite_negative_float(): assert minimal(floats(), lambda x: x < -sys.float_info.max) # Flakey under CrossHair; see https://github.com/pschanely/hypothesis-crosshair/issues/28 @xfail_on_crosshair(Why.undiscovered, strict=False) def test_can_minimal_float_on_boundary_of_representable(): minimal(floats(), lambda x: x + 1 == x and not math.isinf(x)) def test_minimize_nan(): assert math.isnan(minimal(floats(), math.isnan)) def test_minimize_very_large_float(): t = sys.float_info.max / 2 assert minimal(floats(), lambda x: x >= t) == t def is_integral(value): try: return int(value) == value except (OverflowError, ValueError): return False def test_can_minimal_float_far_from_integral(): minimal(floats(), lambda x: math.isfinite(x) and not is_integral(x * (2**32))) def test_list_of_fractional_float(): assert set( minimal( lists(floats(), min_size=5), lambda x: len([t for t in x if t >= 1.5]) >= 5, ) ) == {2} def test_minimal_fractional_float(): assert minimal(floats(), lambda x: x >= 1.5) == 2 @xfail_on_crosshair(Why.undiscovered) # Ineffective CrossHair decision heuristics here def test_minimizes_lists_of_negative_ints_up_to_boundary(): result = minimal( lists(integers(), min_size=10), lambda x: len([t for t in x if t <= -1]) >= 10, ) assert result == [-1] * 10 @pytest.mark.parametrize( ("left", "right"), [(0.0, 5e-324), (-5e-324, 0.0), (-5e-324, 5e-324), (5e-324, 1e-323)], ) def test_floats_in_constrained_range(left, right): @given(floats(left, right)) def test_in_range(r): assert left <= r <= right test_in_range() def test_bounds_are_respected(): assert minimal(floats(min_value=1.0)) == 1.0 assert minimal(floats(max_value=-1.0)) == -1.0 @pytest.mark.parametrize("k", range(10)) def test_floats_from_zero_have_reasonable_range(k): n = 10**k assert minimal(floats(min_value=0.0), lambda x: x >= n) == float(n) assert minimal(floats(max_value=0.0), lambda x: x <= -n) == float(-n) def test_explicit_allow_nan(): minimal(floats(allow_nan=True), math.isnan) def test_one_sided_contains_infinity(): minimal(floats(min_value=1.0), math.isinf) minimal(floats(max_value=1.0), math.isinf) @given(floats(min_value=0.0, allow_infinity=False)) def test_no_allow_infinity_upper(x): assert not math.isinf(x) @given(floats(max_value=0.0, allow_infinity=False)) def test_no_allow_infinity_lower(x): assert not math.isinf(x) class TestFloatsAreFloats: @given(floats()) def test_unbounded(self, arg): assert isinstance(arg, float) @given(floats(min_value=0, max_value=float(2**64 - 1))) def test_int_float(self, arg): assert isinstance(arg, float) @given(floats(min_value=float(0), max_value=float(2**64 - 1))) def test_float_float(self, arg): assert isinstance(arg, float) ================================================ FILE: hypothesis-python/tests/nocover/test_simple_strings.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import unicodedata import pytest from hypothesis import given, settings from hypothesis.strategies import text @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~10 minutes; many-valued realization is slow", ) @given(text(min_size=1, max_size=1)) @settings(max_examples=2000) def test_does_not_generate_surrogates(t): assert unicodedata.category(t) != "Cs" ================================================ FILE: hypothesis-python/tests/nocover/test_skipping.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import unittest import pytest from _pytest.outcomes import Skipped from hypothesis import given, settings from hypothesis.core import skip_exceptions_to_reraise from hypothesis.database import InMemoryExampleDatabase from hypothesis.strategies import integers from tests.common.utils import capture_out @pytest.mark.parametrize("skip_exception", skip_exceptions_to_reraise()) def test_no_falsifying_example_if_unittest_skip(skip_exception): """If a ``SkipTest`` exception is raised during a test, Hypothesis should not continue running the test and shrink process, nor should it print anything about falsifying examples.""" class DemoTest(unittest.TestCase): @given(xs=integers()) def test_to_be_skipped(self, xs): if xs == 0: raise skip_exception else: assert xs == 0 with capture_out() as o: suite = unittest.defaultTestLoader.loadTestsFromTestCase(DemoTest) unittest.TextTestRunner().run(suite) assert "Falsifying example" not in o.getvalue() def test_skip_exceptions_save_database_entries(): """Skip exceptions should save database entries for immediate replay (issue #4484).""" database = InMemoryExampleDatabase() call_count = 0 skip_value = None first_value = None @settings(database=database, max_examples=100) @given(integers()) def test_func(n): nonlocal call_count, skip_value, first_value call_count += 1 if first_value is None: first_value = n # Skip on the 5th value in the first run (the choice of 5 is arbitrary) if call_count == 5 and skip_value is None: skip_value = n if n == skip_value: pytest.skip() # First run should raise a skip exception and save to database with pytest.raises(Skipped): test_func() assert sum(len(v) for v in database.data.values()) == 1 # Second run should immediately replay the skip value first_value = None call_count = 0 with pytest.raises(Skipped): test_func() # first call should be the replayed skip value assert first_value == skip_value assert call_count == 1 ================================================ FILE: hypothesis-python/tests/nocover/test_stateful.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import inspect from collections import namedtuple import pytest from hypothesis import Phase, settings as Settings, strategies as st from hypothesis.stateful import ( Bundle, RuleBasedStateMachine, invariant, precondition, rule, run_state_machine_as_test, ) from tests.common.utils import Why def run_to_notes(TestClass): TestCase = TestClass.TestCase # don't add explain phase notes to the error TestCase.settings = Settings(phases=set(Phase) - {Phase.explain}, max_examples=500) try: TestCase().runTest() except AssertionError as err: return err.__notes__ raise RuntimeError("Expected an assertion error") def assert_runs_to_output(TestClass, output): # remove the first line, which is always "Falsfying example:" actual = "\n".join(run_to_notes(TestClass)[1:]) assert actual == inspect.cleandoc(output.strip()) Leaf = namedtuple("Leaf", ("label",)) Split = namedtuple("Split", ("left", "right")) class BalancedTrees(RuleBasedStateMachine): trees = Bundle("BinaryTree") @rule(target=trees, x=st.booleans()) def leaf(self, x): return Leaf(x) @rule(target=trees, left=trees, right=trees) def split(self, left, right): return Split(left, right) @rule(tree=trees) def test_is_balanced(self, tree): if isinstance(tree, Leaf): return assert abs(self.size(tree.left) - self.size(tree.right)) <= 1 self.test_is_balanced(tree.left) self.test_is_balanced(tree.right) def size(self, tree): if isinstance(tree, Leaf): return 1 else: return 1 + self.size(tree.left) + self.size(tree.right) class DepthCharge: def __init__(self, value): if value is None: self.depth = 0 else: self.depth = value.depth + 1 class DepthMachine(RuleBasedStateMachine): charges = Bundle("charges") @rule(targets=(charges,), child=charges) def charge(self, child): return DepthCharge(child) @rule(targets=(charges,)) def none_charge(self): return DepthCharge(None) @rule(check=charges) def is_not_too_deep(self, check): assert check.depth < 3 class RoseTreeStateMachine(RuleBasedStateMachine): nodes = Bundle("nodes") @rule(target=nodes, source=st.lists(nodes)) def bunch(self, source): return source @rule(source=nodes) def shallow(self, source): def depth(ls): return 0 if not ls else 1 + max(map(depth, ls)) assert depth(source) <= 5 class NotTheLastMachine(RuleBasedStateMachine): stuff = Bundle("stuff") def __init__(self): super().__init__() self.last = None self.bye_called = False @rule(target=stuff) def hi(self): result = object() self.last = result return result @precondition(lambda self: not self.bye_called) @rule(v=stuff) def bye(self, v): assert v == self.last self.bye_called = True class PopulateMultipleTargets(RuleBasedStateMachine): b1 = Bundle("b1") b2 = Bundle("b2") @rule(targets=(b1, b2)) def populate(self): return 1 @rule(x=b1, y=b2) def fail(self, x, y): raise AssertionError class CanSwarm(RuleBasedStateMachine): """This test will essentially never pass if you choose rules uniformly at random, because every time the snake rule fires we return to the beginning, so we will tend to undo progress well before we make enough progress for the test to fail. This tests our swarm testing functionality in stateful testing by ensuring that we can sometimes generate long runs of steps which exclude a particular rule. """ def __init__(self): super().__init__() self.seen = set() @rule(move=st.integers()) def ladder(self, move): self.seen.add(move) assert len(self.seen) <= 15 @rule() def snake(self): self.seen.clear() bad_machines = ( BalancedTrees, DepthMachine, RoseTreeStateMachine, NotTheLastMachine, PopulateMultipleTargets, CanSwarm, ) for m in bad_machines: m.TestCase.settings = Settings( m.TestCase.settings, max_examples=1000, database=None, phases=set(Phase) - {Phase.shrink}, ) @pytest.mark.parametrize( "machine", bad_machines, ids=[t.__name__ for t in bad_machines] ) def test_bad_machines_fail(machine): if ( machine in [CanSwarm, RoseTreeStateMachine] and Settings.get_current_profile_name() == "crosshair" ): # and also takes 10/6 minutes respectively, on top of not finding the failure pytest.xfail(reason=str(Why.undiscovered)) with pytest.raises(AssertionError): machine.TestCase().runTest() class MyStatefulMachine(RuleBasedStateMachine): def __init__(self): self.n_steps = 0 super().__init__() @rule() def step(self): self.n_steps += 1 assert self.n_steps <= 10 class TestMyStatefulMachine(MyStatefulMachine.TestCase): settings = Settings(derandomize=True, stateful_step_count=5) def test_multiple_precondition_bug(): # See https://github.com/HypothesisWorks/hypothesis/issues/2861 class MultiplePreconditionMachine(RuleBasedStateMachine): @rule(x=st.integers()) def good_method(self, x): pass @precondition(lambda self: True) @precondition(lambda self: False) @rule(x=st.integers()) def bad_method_a(self, x): raise AssertionError("This rule runs, even though it shouldn't.") @precondition(lambda self: False) @precondition(lambda self: True) @rule(x=st.integers()) def bad_method_b(self, x): raise AssertionError("This rule might be skipped for the wrong reason.") @precondition(lambda self: True) @rule(x=st.integers()) @precondition(lambda self: False) def bad_method_c(self, x): raise AssertionError("This rule runs, even though it shouldn't.") @rule(x=st.integers()) @precondition(lambda self: True) @precondition(lambda self: False) def bad_method_d(self, x): raise AssertionError("This rule runs, even though it shouldn't.") @precondition(lambda self: True) @precondition(lambda self: False) @invariant() def bad_invariant_a(self): raise AssertionError("This invariant runs, even though it shouldn't.") @precondition(lambda self: False) @precondition(lambda self: True) @invariant() def bad_invariant_b(self): raise AssertionError("This invariant runs, even though it shouldn't.") @precondition(lambda self: True) @invariant() @precondition(lambda self: False) def bad_invariant_c(self): raise AssertionError("This invariant runs, even though it shouldn't.") @invariant() @precondition(lambda self: True) @precondition(lambda self: False) def bad_invariant_d(self): raise AssertionError("This invariant runs, even though it shouldn't.") run_state_machine_as_test(MultiplePreconditionMachine) class UnrelatedCall(RuleBasedStateMachine): a = Bundle("a") def __init__(self): super().__init__() self.calls = set() @rule(target=a, a=st.integers()) def add_a(self, a): self.calls.add("add") return a @rule(v=a) def f(self, v): self.calls.add("f") @precondition(lambda self: "add" in self.calls) @rule(value=st.integers()) def unrelated(self, value): self.calls.add("unrelated") @rule() def invariant(self): # force all three calls to be made in a particular order (with the # `unrelated` precondition) so we always shrink to a particular counterexample. assert len(self.calls) != 3 def test_unrelated_rule_does_not_use_var_reference_repr(): # we are specifically looking for state.unrelated(value=0) not being replaced # with state.unrelated(value=a_0). The `unrelated` rule is drawing from # st.integers, not a bundle, so the values should not be conflated even if # they're both 0. assert_runs_to_output( UnrelatedCall, """ state = UnrelatedCall() a_0 = state.add_a(a=0) state.f(v=a_0) state.unrelated(value=0) state.invariant() state.teardown() """, ) class SourceSameAsTarget(RuleBasedStateMachine): values = Bundle("values") @rule(target=values, value=st.lists(values)) def f(self, value): assert len(value) == 0 return value class SourceSameAsTargetUnclearOrigin(RuleBasedStateMachine): values = Bundle("values") def __init__(self): super().__init__() self.called = False @rule(target=values, value=st.just([]) | st.lists(values)) def f(self, value): assert not self.called # ensure we get two calls to f before failing. In the minimal failing # example, both will be from st.just([]). self.called = True return value def test_replaces_when_same_id(): assert_runs_to_output( SourceSameAsTarget, f""" state = {SourceSameAsTarget.__name__}() values_0 = state.f(value=[]) state.f(value=[values_0]) state.teardown() """, ) def test_doesnt_replace_when_different_id(): assert_runs_to_output( SourceSameAsTargetUnclearOrigin, f""" state = {SourceSameAsTargetUnclearOrigin.__name__}() values_0 = state.f(value=[]) state.f(value=[]) state.teardown() """, ) ================================================ FILE: hypothesis-python/tests/nocover/test_strategy_state.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import hashlib import math from random import Random from hypothesis import Verbosity, assume, settings from hypothesis.database import InMemoryExampleDatabase from hypothesis.internal.compat import PYPY from hypothesis.internal.floats import clamp, float_to_int, int_to_float, is_negative from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule from hypothesis.strategies import ( binary, booleans, complex_numbers, data, decimals, floats, fractions, integers, just, lists, none, sampled_from, text, tuples, ) class HypothesisSpec(RuleBasedStateMachine): def __init__(self): super().__init__() self.database = None strategies = Bundle("strategy") strategy_tuples = Bundle("tuples") objects = Bundle("objects") basic_data = Bundle("basic") varied_floats = Bundle("varied_floats") def teardown(self): self.clear_database() @rule() def clear_database(self): if self.database is not None: self.database = None @rule() def set_database(self): self.teardown() self.database = InMemoryExampleDatabase() @rule( target=strategies, spec=sampled_from( ( integers(), booleans(), floats(), complex_numbers(), fractions(), decimals(), text(), binary(), none(), tuples(), ) ), ) def strategy(self, spec): return spec @rule(target=strategies, values=lists(integers() | text(), min_size=1)) def sampled_from_strategy(self, values): return sampled_from(values) @rule(target=strategies, spec=strategy_tuples) def strategy_for_tupes(self, spec): return tuples(*spec) @rule(target=strategies, source=strategies, level=integers(1, 10), mixer=text()) def filtered_strategy(self, source, level, mixer): def is_good(x): seed = hashlib.sha384((mixer + repr(x)).encode()).digest() return bool(Random(seed).randint(0, level)) return source.filter(is_good) @rule(target=strategies, elements=strategies) def list_strategy(self, elements): return lists(elements) @rule(target=strategies, left=strategies, right=strategies) def or_strategy(self, left, right): return left | right @rule(target=varied_floats, source=floats()) def float(self, source): return source @rule(target=varied_floats, source=varied_floats, offset=integers(-100, 100)) def adjust_float(self, source, offset): return int_to_float(clamp(0, float_to_int(source) + offset, 2**64 - 1)) @rule(target=strategies, left=varied_floats, right=varied_floats) def float_range(self, left, right): assume(math.isfinite(left) and math.isfinite(right)) left, right = sorted((left, right)) assert left <= right # exclude deprecated case where left = 0.0 and right = -0.0 assume(left or right or not (is_negative(right) and not is_negative(left))) return floats(left, right) @rule( target=strategies, source=strategies, result1=strategies, result2=strategies, mixer=text(), p=floats(0, 1), ) def flatmapped_strategy(self, source, result1, result2, mixer, p): assume(result1 is not result2) def do_map(value): rep = repr(value) random = Random(hashlib.sha384((mixer + rep).encode()).digest()) if random.random() <= p: return result1 else: return result2 return source.flatmap(do_map) @rule(target=strategies, value=objects) def just_strategy(self, value): return just(value) @rule(target=strategy_tuples, source=strategies) def single_tuple(self, source): return (source,) @rule(target=strategy_tuples, left=strategy_tuples, right=strategy_tuples) def cat_tuples(self, left, right): return left + right @rule(target=objects, strat=strategies, data=data()) def get_example(self, strat, data): data.draw(strat) @rule(target=strategies, left=integers(), right=integers()) def integer_range(self, left, right): left, right = sorted((left, right)) return integers(left, right) @rule(strat=strategies) def repr_is_good(self, strat): assert " at 0x" not in repr(strat) MAIN = __name__ == "__main__" TestHypothesis = HypothesisSpec.TestCase TestHypothesis.settings = settings( TestHypothesis.settings, stateful_step_count=10 if PYPY else 50, verbosity=max(TestHypothesis.settings.verbosity, Verbosity.verbose), max_examples=10000 if MAIN else 200, ) if MAIN: TestHypothesis().runTest() ================================================ FILE: hypothesis-python/tests/nocover/test_subnormal_floats.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math from sys import float_info import pytest from hypothesis.internal.floats import width_smallest_normals from hypothesis.strategies import floats from tests.common.debug import assert_all_examples, find_any from tests.common.utils import PYTHON_FTZ def test_python_compiled_with_sane_math_options(): """Python does not flush-to-zero, which violates IEEE-754 The other tests that rely on subnormals are skipped when Python is FTZ (otherwise pytest will be very noisy), so this meta test ensures CI jobs still fail as we currently don't care to support such builds of Python. """ assert not PYTHON_FTZ skipif_ftz = pytest.mark.skipif(PYTHON_FTZ, reason="broken by unsafe compiler flags") @skipif_ftz def test_can_generate_subnormals(): find_any(floats().filter(lambda x: x > 0), lambda x: x < float_info.min) find_any(floats().filter(lambda x: x < 0), lambda x: x > -float_info.min) @skipif_ftz @pytest.mark.parametrize( "min_value, max_value", [(None, None), (-1, 0), (0, 1), (-1, 1)] ) @pytest.mark.parametrize("width", [16, 32, 64]) def test_does_not_generate_subnormals_when_disallowed(width, min_value, max_value): strat = floats( min_value=min_value, max_value=max_value, allow_subnormal=False, width=width, ) strat = strat.filter(lambda x: x != 0.0 and math.isfinite(x)) smallest_normal = width_smallest_normals[width] assert_all_examples(strat, lambda x: x <= -smallest_normal or x >= smallest_normal) ================================================ FILE: hypothesis-python/tests/nocover/test_targeting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import Phase, given, seed, settings, strategies as st, target from tests.common.utils import Why, xfail_on_crosshair pytest_plugins = "pytester" TESTSUITE = """ from hypothesis import given, strategies as st, target @given(st.integers(min_value=0)) def test_threshold_problem(x): target(float(x)) {0}target(float(x * 2), label="double") {0}assert x <= 100000 assert x <= 100 """ @pytest.mark.parametrize("multiple", [False, True]) def test_reports_target_results(testdir, multiple): script = testdir.makepyfile(TESTSUITE.format("" if multiple else "# ")) result = testdir.runpytest(script, "--tb=native", "-rN") out = "\n".join(result.stdout.lines) assert "Falsifying example" in out assert "x=101" in out, out assert out.count("Highest target score") == 1 assert result.ret != 0 def test_targeting_increases_max_length(): strat = st.lists(st.booleans()) @settings(database=None, max_examples=200, phases=[Phase.generate, Phase.target]) @given(strat) def test_with_targeting(ls): target(float(len(ls))) assert len(ls) <= 80 with pytest.raises(AssertionError): test_with_targeting() @given(st.integers(), st.integers()) def test_target_returns_value(a, b): difference = target(abs(a - b)) assert difference == abs(a - b) assert isinstance(difference, int) @xfail_on_crosshair(Why.symbolic_outside_context) def test_targeting_can_be_disabled(): strat = st.lists(st.integers(0, 255)) def score(enabled): result = 0 phases = [Phase.generate] if enabled: phases.append(Phase.target) @seed(0) @settings(database=None, max_examples=100, phases=phases) @given(strat) def test(ls): nonlocal result # cap the score to avoid long test times by unbounded driving of list # length upwards score = min(sum(ls), 10_000) result = max(result, score) target(score) test() return result assert score(enabled=True) > score(enabled=False) @pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="takes ~15 minutes, mostly just unrolling the rejection sampling loop", ) def test_issue_2395_regression(): @given(d=st.floats().filter(lambda x: abs(x) < 1000)) @settings(max_examples=1000, database=None) @seed(93962505385993024185959759429298090872) def test_targeting_square_loss(d): target(-((d - 42.5) ** 2.0)) test_targeting_square_loss() ================================================ FILE: hypothesis-python/tests/nocover/test_testdecorators.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import pytest from hypothesis import HealthCheck, given, reject, settings, strategies as st from hypothesis.errors import InvalidArgument, Unsatisfiable def test_contains_the_test_function_name_in_the_exception_string(): look_for_one = settings(max_examples=1, suppress_health_check=list(HealthCheck)) @given(st.integers()) @look_for_one def this_has_a_totally_unique_name(x): reject() with pytest.raises( Unsatisfiable, match=re.escape(this_has_a_totally_unique_name.__name__) ): this_has_a_totally_unique_name() class Foo: @given(st.integers()) @look_for_one def this_has_a_unique_name_and_lives_on_a_class(self, x): reject() with pytest.raises( Unsatisfiable, match=re.escape(Foo.this_has_a_unique_name_and_lives_on_a_class.__name__), ): Foo().this_has_a_unique_name_and_lives_on_a_class() def test_signature_mismatch_error_message(): # Regression test for issue #1978 @settings(max_examples=2) @given(x=st.integers()) def bad_test(): pass with pytest.raises( InvalidArgument, match=r"bad_test\(\) got an unexpected keyword argument 'x', " r"from `x=integers\(\)` in @given", ): bad_test() @given(data=st.data(), keys=st.lists(st.integers(), unique=True)) def test_fixed_dict_preserves_iteration_order(data, keys): d = data.draw(st.fixed_dictionaries({k: st.none() for k in keys})) assert all(a == b for a, b in zip(keys, d, strict=True)), f"{keys=}, {d.keys()=}" ================================================ FILE: hypothesis-python/tests/nocover/test_threading.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys import time from threading import Barrier, Thread import pytest from hypothesis import given, settings, strategies as st from hypothesis.errors import DeadlineExceeded, InvalidArgument from hypothesis.internal.conjecture.junkdrawer import ensure_free_stackframes from hypothesis.stateful import RuleBasedStateMachine, invariant, rule from hypothesis.strategies import SearchStrategy from tests.common.debug import check_can_generate_examples from tests.common.utils import run_concurrently pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="crosshair is not thread safe", ) def test_can_run_given_in_thread(): has_run_successfully = False @given(st.integers()) def test(n): nonlocal has_run_successfully has_run_successfully = True t = Thread(target=test) t.start() t.join() assert has_run_successfully def test_run_stateful_test_concurrently(): class MyStateMachine(RuleBasedStateMachine): def __init__(self): super().__init__() @rule(n=st.integers()) def my_rule(self, n): pass @invariant() def my_invariant(self): pass TestMyStateful = MyStateMachine.TestCase().runTest run_concurrently(TestMyStateful, n=2) def do_work(*, multiplier=1): # arbitrary moderately-expensive work for x in range(500 * multiplier): _y = x**x def test_run_different_tests_in_threads(): @given(st.integers()) def test1(n): do_work() @given(st.integers()) def test2(n): do_work() thread1 = Thread(target=test1) thread2 = Thread(target=test2) thread1.start() thread2.start() thread1.join() thread2.join() def test_run_given_concurrently(): @given(st.data(), st.integers(-5, 5).map(lambda x: 10**x)) def test(data, magnitude): assert magnitude != 0 data.draw(st.complex_numbers(max_magnitude=magnitude)) run_concurrently(test, n=2) def test_stackframes_restores_original_recursion_limit(): original_recursionlimit = sys.getrecursionlimit() def test(): with ensure_free_stackframes(): do_work() # also mix in a hypothesis test; why not. @given(st.integers()) @settings(max_examples=10) def test(n): pass test() threads = [] for _ in range(4): threads.append(Thread(target=test)) for thread in threads: thread.start() for thread in threads: thread.join(timeout=10) assert sys.getrecursionlimit() == original_recursionlimit @pytest.mark.parametrize( "strategy", [ st.recursive(st.none(), st.lists, max_leaves=-1), st.recursive(st.none(), st.lists, max_leaves=0), st.recursive(st.none(), st.lists, max_leaves=1.0), ], ) def test_handles_invalid_args_cleanly(strategy): # we previously had a race in SearchStrategy.validate, where one thread would # set `validate_called = True` (which it has to do first for recursive # strategies), then another thread would try to generate before the validation # finished and errored, and would get into weird technically-valid states # like interpreting 1.0 as 1. I saw FlakyStrategyDefinition here because the # validating + errored thread drew zero choices, but the other thread drew # 1 choice, for the same shared strategy. def check(): with pytest.raises(InvalidArgument): check_can_generate_examples(strategy) run_concurrently(check, n=4) def test_single_thread_can_raise_deadline_exceeded(): # a slow test running inside a thread, but not concurrently, should still # be able to raise DeadlineExceeded. @given(st.integers()) @settings(max_examples=5) def slow_test(n): do_work() time.sleep(0.4) def target(): with pytest.raises(DeadlineExceeded): slow_test() thread = Thread(target=target) thread.start() thread.join(timeout=10) def test_deadline_exceeded_not_raised_under_concurrent_threads(): # it's still possible for multithreaded calls to a slow function to raise # DeadlineExceeded, if the first thread completes its entire test before # any other thread starts. For this test, prevent this scenario with a barrier, # forcing the threads to run in parallel. n_threads = 8 barrier = Barrier(n_threads) @given(st.integers()) @settings(max_examples=5) def slow_test(n): do_work() time.sleep(0.4) barrier.wait() run_concurrently(slow_test, n=n_threads) def test_deadline_exceeded_can_be_raised_after_threads(): # if we had concurrent threads before, but they've finished now, we should # still be able to raise DeadlineExceeded normally. Importantly, we test this # for the same test as was running before, since concurrent thread use is # tracked per-@given. @given(st.integers()) @settings(max_examples=5) def slow_test(n): do_work() if should_sleep: time.sleep(0.4) should_sleep = False run_concurrently(slow_test, n=8) should_sleep = True with pytest.raises(DeadlineExceeded): slow_test() def test_one_of_branches_lock(): class SlowBranchesStrategy(SearchStrategy): @property def branches(self): # multiplier=2 reproduces more consistently than multiplier=1 for me do_work(multiplier=2) return [st.integers(), st.text()] branch_counts = set() s = st.one_of(SlowBranchesStrategy(), SlowBranchesStrategy()) def test(): branches = len(s.branches) branch_counts.add(branches) run_concurrently(test, n=10) assert len(branch_counts) == 1 # there are 4 independent strategies, but only 2 distinct ones - # st.integers(), and st.text(). assert branch_counts == {2} ================================================ FILE: hypothesis-python/tests/nocover/test_type_lookup.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Callable, Collection, Sequence from typing import Concatenate, ParamSpec import pytest from hypothesis import strategies as st from hypothesis.errors import InvalidArgument from hypothesis.strategies._internal.types import NON_RUNTIME_TYPES from tests.common.debug import ( assert_simple_property, check_can_generate_examples, find_any, ) try: from typing import TypeGuard # new in 3.10 except ImportError: TypeGuard = None try: from typing import TypeIs # new in 3.13 except ImportError: TypeIs = None @pytest.mark.parametrize("non_runtime_type", NON_RUNTIME_TYPES) def test_non_runtime_type_cannot_be_resolved(non_runtime_type): strategy = st.from_type(non_runtime_type) with pytest.raises( InvalidArgument, match="there is no such thing as a runtime instance" ): check_can_generate_examples(strategy) @pytest.mark.parametrize("non_runtime_type", NON_RUNTIME_TYPES) def test_non_runtime_type_cannot_be_registered(non_runtime_type): with pytest.raises( InvalidArgument, match="there is no such thing as a runtime instance" ): st.register_type_strategy(non_runtime_type, st.none()) @pytest.mark.skipif(Concatenate is None, reason="requires python3.10 or higher") def test_callable_with_concatenate(): P = ParamSpec("P") func_type = Callable[Concatenate[int, P], None] strategy = st.from_type(func_type) with pytest.raises( InvalidArgument, match="Hypothesis can't yet construct a strategy for instances of a Callable type", ): check_can_generate_examples(strategy) with pytest.raises(InvalidArgument, match="Cannot register generic type"): st.register_type_strategy(func_type, st.none()) @pytest.mark.skipif(ParamSpec is None, reason="requires python3.10 or higher") def test_callable_with_paramspec(): P = ParamSpec("P") func_type = Callable[P, None] strategy = st.from_type(func_type) with pytest.raises( InvalidArgument, match="Hypothesis can't yet construct a strategy for instances of a Callable type", ): check_can_generate_examples(strategy) with pytest.raises(InvalidArgument, match="Cannot register generic type"): st.register_type_strategy(func_type, st.none()) @pytest.mark.parametrize("typ", [TypeGuard, TypeIs]) def test_callable_return_typegard_type(typ): if typ is None: pytest.skip("Requires modern typing") strategy = st.from_type(Callable[[], typ[int]]) with pytest.raises( InvalidArgument, match="Hypothesis cannot yet construct a strategy for callables " "which are PEP-647 TypeGuards or PEP-742 TypeIs", ): check_can_generate_examples(strategy) with pytest.raises(InvalidArgument, match="Cannot register generic type"): st.register_type_strategy(Callable[[], typ[int]], st.none()) def test_binary_type_resolution(): # we have special logic for this, see # https://github.com/HypothesisWorks/hypothesis/pull/4490 find_any(st.from_type(Collection[int]), lambda v: isinstance(v, bytes)) find_any(st.from_type(Sequence[object]), lambda v: isinstance(v, bytes)) find_any(st.from_type(Sequence[int]), lambda v: isinstance(v, bytes)) assert_simple_property(st.from_type(type[int]), lambda v: not isinstance(v, bytes)) assert_simple_property(st.from_type(set[int]), lambda v: not isinstance(v, bytes)) ================================================ FILE: hypothesis-python/tests/nocover/test_type_lookup_forward_ref.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from typing import Dict as _Dict, ForwardRef, Union import pytest from hypothesis import given, settings, strategies as st from hypothesis.errors import ResolutionFailed from tests.common import utils from tests.common.utils import skipif_threading # error only occurs with typing variants # ruff: noqa: UP006, UP035, UP007 # Mutually-recursive types # See https://github.com/HypothesisWorks/hypothesis/issues/2722 pytestmark = pytest.mark.skipif( settings.get_current_profile_name() == "crosshair", reason="slow with recursive strustures: https://github.com/pschanely/hypothesis-crosshair/issues/27", ) @skipif_threading # weird errors around b_strategy scope? @given(st.data()) def test_mutually_recursive_types_with_typevar(data): # The previously-failing example from the issue A = _Dict[bool, "B"] B = Union[list[bool], A] with pytest.raises(ResolutionFailed, match=r"Could not resolve ForwardRef\('B'\)"): data.draw(st.from_type(A)) with utils.temp_registered( ForwardRef("B"), lambda _: st.deferred(lambda: b_strategy), ): b_strategy = st.from_type(B) data.draw(b_strategy) data.draw(st.from_type(A)) data.draw(st.from_type(B)) @skipif_threading # weird errors around d_strategy scope? @given(st.data()) def test_mutually_recursive_types_with_typevar_alternate(data): # It's not particularly clear why this version passed when the previous # test failed, but different behaviour means we add both to the suite. C = Union[list[bool], "D"] D = dict[bool, C] with pytest.raises(ResolutionFailed, match=r"Could not resolve ForwardRef\('D'\)"): data.draw(st.from_type(C)) with utils.temp_registered( ForwardRef("D"), lambda _: st.deferred(lambda: d_strategy), ): d_strategy = st.from_type(D) data.draw(d_strategy) data.draw(st.from_type(C)) data.draw(st.from_type(D)) ================================================ FILE: hypothesis-python/tests/nocover/test_type_lookup_future_annotations.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations from typing import TypedDict import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument from tests.common.debug import check_can_generate_examples alias = int | str class A(TypedDict): a: int class B(TypedDict): a: A b: alias @given(st.from_type(B)) def test_complex_forward_ref_in_typed_dict(d): assert isinstance(d["a"], dict) assert isinstance(d["a"]["a"], int) assert isinstance(d["b"], (int, str)) def test_complex_forward_ref_in_typed_dict_local(): local_alias = int | str class C(TypedDict): a: A b: local_alias c_strategy = st.from_type(C) with pytest.raises(InvalidArgument): check_can_generate_examples(c_strategy) ================================================ FILE: hypothesis-python/tests/nocover/test_unusual_settings_configs.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import HealthCheck, Verbosity, assume, given, settings, strategies as st @settings(max_examples=1, database=None) @given(st.integers()) def test_single_example(n): pass @settings( max_examples=1, database=None, suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], verbosity=Verbosity.debug, ) @given(st.integers()) def test_hard_to_find_single_example(n): # Numbers are arbitrary, just deliberately unlikely to hit this too soon. assume(n % 50 == 11) ================================================ FILE: hypothesis-python/tests/nocover/test_uuids.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given, strategies as st from tests.common.debug import minimal @given(st.lists(st.uuids())) def test_are_unique(ls): assert len(set(ls)) == len(ls) def test_retains_uniqueness_in_simplify(): ts = minimal(st.lists(st.uuids()), lambda x: len(x) >= 5) assert len(ts) == len(set(ts)) == 5 @pytest.mark.parametrize("version", (1, 2, 3, 4, 5)) def test_can_generate_specified_version(version): @given(st.uuids(version=version)) def inner(uuid): assert version == uuid.version inner() ================================================ FILE: hypothesis-python/tests/numpy/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. try: EncodingWarning except NameError: pass else: # Work around https://github.com/numpy/numpy/issues/24115 import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", EncodingWarning) import numpy.testing # noqa ================================================ FILE: hypothesis-python/tests/numpy/test_argument_validation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import numpy import pytest from hypothesis import strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra import numpy as nps from tests.common.debug import check_can_generate_examples from tests.common.utils import checks_deprecated_behaviour def e(a, **kwargs): kw = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) return pytest.param(a, kwargs, id=f"{a.__name__}({kw})") @pytest.mark.parametrize( ("function", "kwargs"), [ e(nps.array_dtypes, min_size=2, max_size=1), e(nps.array_dtypes, min_size=-1), e(nps.array_shapes, min_side=2, max_side=1), e(nps.array_shapes, min_dims=3, max_dims=2), e(nps.array_shapes, min_dims=-1), e(nps.array_shapes, min_side=-1), e(nps.array_shapes, min_side="not an int"), e(nps.array_shapes, max_side="not an int"), e(nps.array_shapes, min_dims="not an int"), e(nps.array_shapes, max_dims="not an int"), e(nps.array_shapes, min_dims=33), e(nps.array_shapes, max_dims=33), e(nps.arrays, dtype=float, shape=(0.5,)), e(nps.arrays, dtype=numpy.void, shape=1), e(nps.arrays, dtype=float, shape=1, fill=3), e(nps.arrays, dtype="U", shape=1, elements=st.just("abc\0\0")), e(nps.arrays, dtype=int, shape=1, elements="not a strategy"), e(nps.byte_string_dtypes, min_len=-1), e(nps.byte_string_dtypes, min_len=2, max_len=1), e(nps.byte_string_dtypes, min_len=0, max_len=0), e(nps.datetime64_dtypes, max_period=11), e(nps.datetime64_dtypes, min_period=11), e(nps.datetime64_dtypes, min_period="Y", max_period="M"), e(nps.timedelta64_dtypes, max_period=11), e(nps.timedelta64_dtypes, min_period=11), e(nps.timedelta64_dtypes, min_period="Y", max_period="M"), e(nps.unicode_string_dtypes, min_len=-1), e(nps.unicode_string_dtypes, min_len=2, max_len=1), e(nps.unicode_string_dtypes, min_len=0, max_len=0), e(nps.unsigned_integer_dtypes, endianness=3), e(nps.unsigned_integer_dtypes, sizes=()), e(nps.unsigned_integer_dtypes, sizes=(3,)), e(nps.from_dtype, dtype="float64"), e(nps.from_dtype, dtype=float), e(nps.from_dtype, dtype=numpy.int8), e(nps.from_dtype, dtype=1), e(nps.from_dtype, dtype=numpy.dtype("uint8"), min_value=-999), e(nps.from_dtype, dtype=numpy.dtype("uint8"), max_value=999), e(nps.from_dtype, dtype=numpy.dtype("int8"), min_value=-999), e(nps.from_dtype, dtype=numpy.dtype("int8"), max_value=999), e(nps.from_dtype, dtype=numpy.dtype("S4"), max_size=5), e(nps.from_dtype, dtype=numpy.dtype("U4"), max_size=5), e(nps.valid_tuple_axes, ndim=-1), e(nps.valid_tuple_axes, ndim=2, min_size=-1), e(nps.valid_tuple_axes, ndim=2, min_size=3, max_size=10), e(nps.valid_tuple_axes, ndim=2, min_size=2, max_size=1), e(nps.valid_tuple_axes, ndim=2.0, min_size=2, max_size=1), e(nps.valid_tuple_axes, ndim=2, min_size=1.0, max_size=2), e(nps.valid_tuple_axes, ndim=2, min_size=1, max_size=2.0), e(nps.valid_tuple_axes, ndim=2, min_size=1, max_size=3), e(nps.broadcastable_shapes, shape="a"), e(nps.broadcastable_shapes, shape=(2, 2), min_side="a"), e(nps.broadcastable_shapes, shape=(2, 2), min_dims="a"), e(nps.broadcastable_shapes, shape=(2, 2), max_side="a"), e(nps.broadcastable_shapes, shape=(2, 2), max_dims="a"), e(nps.broadcastable_shapes, shape=(2, 2), min_side=-1), e(nps.broadcastable_shapes, shape=(2, 2), min_dims=-1), e(nps.broadcastable_shapes, shape=(2, 2), min_dims=33, max_dims=None), e(nps.broadcastable_shapes, shape=(2, 2), min_dims=1, max_dims=33), e(nps.broadcastable_shapes, shape=(2, 2), min_side=1, max_side=0), e(nps.broadcastable_shapes, shape=(2, 2), min_dims=1, max_dims=0), e( nps.broadcastable_shapes, # max_side too small shape=(5, 1), min_dims=2, max_dims=4, min_side=2, max_side=3, ), e( nps.broadcastable_shapes, # min_side too large shape=(0, 1), min_dims=2, max_dims=4, min_side=2, max_side=3, ), e( nps.broadcastable_shapes, # default max_dims unsatisfiable shape=(5, 3, 2, 1), min_dims=3, max_dims=None, min_side=2, max_side=3, ), e( nps.broadcastable_shapes, # default max_dims unsatisfiable shape=(0, 3, 2, 1), min_dims=3, max_dims=None, min_side=2, max_side=3, ), e(nps.mutually_broadcastable_shapes), e(nps.mutually_broadcastable_shapes, num_shapes=0), e(nps.mutually_broadcastable_shapes, num_shapes="a"), e(nps.mutually_broadcastable_shapes, num_shapes=2, base_shape="a"), e( nps.mutually_broadcastable_shapes, # min_side is invalid type num_shapes=2, min_side="a", ), e( nps.mutually_broadcastable_shapes, # min_dims is invalid type num_shapes=2, min_dims="a", ), e( nps.mutually_broadcastable_shapes, # max_side is invalid type num_shapes=2, max_side="a", ), e( nps.mutually_broadcastable_shapes, # max_side is invalid type num_shapes=2, max_dims="a", ), e( nps.mutually_broadcastable_shapes, # min_side is out of domain num_shapes=2, min_side=-1, ), e( nps.mutually_broadcastable_shapes, # min_dims is out of domain num_shapes=2, min_dims=-1, ), e( nps.mutually_broadcastable_shapes, # min_dims is out of domain num_shapes=2, min_dims=33, ), e( nps.mutually_broadcastable_shapes, # max_dims is out of domain num_shapes=2, max_dims=33, ), e( nps.mutually_broadcastable_shapes, # max_side < min_side num_shapes=2, min_side=1, max_side=0, ), e( nps.mutually_broadcastable_shapes, # max_dims < min_dims num_shapes=2, min_dims=1, max_dims=0, ), e( nps.mutually_broadcastable_shapes, # max_side too small num_shapes=2, base_shape=(5, 1), min_dims=2, max_dims=4, min_side=2, max_side=3, ), e( nps.mutually_broadcastable_shapes, # min_side too large num_shapes=2, base_shape=(0, 1), min_dims=2, max_dims=4, min_side=2, max_side=3, ), e( nps.mutually_broadcastable_shapes, # user-specified max_dims unsatisfiable num_shapes=1, base_shape=(5, 3, 2, 1), min_dims=3, max_dims=4, min_side=2, max_side=3, ), e( nps.mutually_broadcastable_shapes, # user-specified max_dims unsatisfiable num_shapes=2, base_shape=(0, 3, 2, 1), min_dims=3, max_dims=4, min_side=2, max_side=3, ), e( nps.mutually_broadcastable_shapes, # valid to pass num_shapes xor gufunc num_shapes=2, signature="()->()", ), e( nps.mutually_broadcastable_shapes, # element-wise ufunc has signature=None signature=numpy.add.signature, ), e( nps.mutually_broadcastable_shapes, # multiple outputs not yet supported signature="()->(),()", ), e( nps.mutually_broadcastable_shapes, # output has dimension not in inputs signature="()->(i)", ), e( nps.mutually_broadcastable_shapes, # frozen-optional is ambiguous & banned signature="(2?)->()", ), e( nps.mutually_broadcastable_shapes, # signature must be in string format signature=([(), ()], [()]), ), e( nps.mutually_broadcastable_shapes, # string must match signature regex signature="this string isn't a valid signature", ), e( nps.mutually_broadcastable_shapes, # shape with too many dimensions signature="(" + ",".join(f"d{n}" for n in range(33)) + ")->()", ), e( nps.mutually_broadcastable_shapes, # max_dims too large given ufunc signature=numpy.matmul.signature, max_dims=32, ), e( nps.mutually_broadcastable_shapes, # least valid max_dims is < min_dims signature=numpy.matmul.signature, min_dims=32, ), e(nps.basic_indices, shape=0), e(nps.basic_indices, shape=("1", "2")), e(nps.basic_indices, shape=(0, -1)), e(nps.basic_indices, shape=(0, 0), allow_newaxis=None), e(nps.basic_indices, shape=(0, 0), allow_ellipsis=None), e(nps.basic_indices, shape=(0, 0), min_dims=-1), e(nps.basic_indices, shape=(0, 0), min_dims=1.0), e(nps.basic_indices, shape=(0, 0), max_dims=-1), e(nps.basic_indices, shape=(0, 0), max_dims=1.0), e(nps.basic_indices, shape=(0, 0), min_dims=2, max_dims=1), e(nps.basic_indices, shape=(3, 3, 3), max_dims="not an int"), e(nps.integer_array_indices, shape=()), e(nps.integer_array_indices, shape=(2, 0)), e(nps.integer_array_indices, shape="a"), e(nps.integer_array_indices, shape=(2,), result_shape=(2, 2)), e(nps.integer_array_indices, shape=(2,), dtype=float), ], ) def test_raise_invalid_argument(function, kwargs): with pytest.raises(InvalidArgument): check_can_generate_examples(function(**kwargs)) @pytest.mark.parametrize( ("function", "kwargs"), [ e(nps.basic_indices, shape=(0, 0), min_dims=50), e(nps.basic_indices, shape=(0, 0), max_dims=50), ], ) @checks_deprecated_behaviour def test_raise_invalid_argument_deprecated(function, kwargs): with pytest.raises(InvalidArgument): check_can_generate_examples(function(**kwargs)) ================================================ FILE: hypothesis-python/tests/numpy/test_deprecation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from warnings import catch_warnings import pytest from hypothesis.errors import HypothesisDeprecationWarning, InvalidArgument from hypothesis.extra import numpy as nps from tests.common.debug import check_can_generate_examples def test_basic_indices_bad_min_dims_warns(): with pytest.warns(HypothesisDeprecationWarning), pytest.raises(InvalidArgument): check_can_generate_examples(nps.basic_indices((3, 3, 3), min_dims=4)) def test_basic_indices_bad_max_dims_warns(): with pytest.warns(HypothesisDeprecationWarning): check_can_generate_examples(nps.basic_indices((3, 3, 3), max_dims=4)) def test_basic_indices_default_max_dims_does_not_warn(): with catch_warnings(record=True) as record: check_can_generate_examples(nps.basic_indices((3, 3, 3))) check_can_generate_examples(nps.basic_indices((3, 3, 3), allow_newaxis=True)) assert len(record) == 0 ================================================ FILE: hypothesis-python/tests/numpy/test_fill_values.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, strategies as st from hypothesis.extra.numpy import arrays from tests.common.debug import find_any, minimal @given(arrays(object, 100, elements=st.builds(list))) def test_generated_lists_are_distinct(ls): assert len(set(map(id, ls))) == len(ls) @st.composite def distinct_integers(draw): used = draw(st.shared(st.builds(set), key="distinct_integers.used")) i = draw(st.integers(0, 2**64 - 1).filter(lambda x: x not in used)) used.add(i) return i @given(arrays("uint64", 10, elements=distinct_integers())) def test_does_not_reuse_distinct_integers(arr): assert len(set(arr)) == len(arr) def test_may_reuse_distinct_integers_if_asked(): find_any( arrays("uint64", 10, elements=distinct_integers(), fill=distinct_integers()), lambda x: len(set(x)) < len(x), ) def test_minimizes_to_fill(): result = minimal(arrays(float, 10, fill=st.just(3.0))) assert (result == 3.0).all() @given( arrays( dtype=float, elements=st.floats(allow_nan=False).filter(bool), shape=(3, 3, 3), fill=st.just(1.0), ) ) def test_fills_everything(x): assert x.all() ================================================ FILE: hypothesis-python/tests/numpy/test_floor_ceil.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import math import numpy as np import pytest from hypothesis.internal.compat import ceil, floor @pytest.mark.parametrize( "value", [ # These are strings so that the test names are easier to read. "2**64+1", "2**64-1", "2**63+1", "2**53+1", "-2**53-1", "-2**63+1", "-2**63-1", "-2**64+1", "-2**64-1", ], ) def test_our_floor_and_ceil_avoid_numpy_rounding(value): a = np.array(eval(value)) f = floor(a) c = ceil(a) # Check *exact* type - we don't want to allow a subclass of int here assert type(f) == int assert type(c) == int # Using math.floor or math.ceil for these values would give an incorrect result. assert (math.floor(a) > a) or (math.ceil(a) < a) assert f <= a <= c assert f + 1 > a > c - 1 ================================================ FILE: hypothesis-python/tests/numpy/test_from_dtype.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys import numpy as np import pytest from hypothesis import assume, given, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra import numpy as nps from hypothesis.internal.floats import width_smallest_normals from hypothesis.strategies._internal import SearchStrategy from tests.common.debug import assert_no_examples, check_can_generate_examples, find_any np_version = tuple(int(x) for x in np.__version__.split(".")[:2]) STANDARD_TYPES = [ np.dtype(t) for t in ( "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float", "float16", "float32", "float64", "complex64", "complex128", "datetime64", "timedelta64", bool, str, bytes, ) ] for nonstandard_typecode in ["g", "G", "S1", "q", "Q"]: try: STANDARD_TYPES.append(np.dtype(nonstandard_typecode)) except Exception: pass @given(nps.nested_dtypes()) def test_strategies_for_standard_dtypes_have_reusable_values(dtype): assert nps.from_dtype(dtype).has_reusable_values @pytest.mark.parametrize("t", STANDARD_TYPES) def test_produces_instances(t): @given(nps.from_dtype(t)) def test_is_t(x): assert isinstance(x, t.type) assert x.dtype.kind == t.kind test_is_t() @settings(max_examples=100, deadline=None) @given(nps.nested_dtypes(max_itemsize=400), st.data()) def test_infer_strategy_from_dtype(dtype, data): # Given a dtype assert isinstance(dtype, np.dtype) # We can infer a strategy strat = nps.from_dtype(dtype) assert isinstance(strat, SearchStrategy) # And use it to fill an array of that dtype data.draw(nps.arrays(dtype, 10, elements=strat)) @given(st.data()) def test_can_cast_for_scalars(data): # Note: this only passes with castable datatypes, certain dtype # combinations will result in an error if numpy is not able to cast them. dt_elements = np.dtype(data.draw(st.sampled_from(["bool", "i2"]))) dt_desired = np.dtype( data.draw(st.sampled_from(["i2", "float32", "float64"])) ) result = data.draw( nps.arrays(dtype=dt_desired, elements=nps.from_dtype(dt_elements), shape=()) ) assert isinstance(result, np.ndarray) assert result.dtype == dt_desired @given(st.data()) def test_unicode_string_dtypes_generate_unicode_strings(data): dt = data.draw(nps.unicode_string_dtypes()) result = data.draw(nps.from_dtype(dt)) assert isinstance(result, str) @given(nps.arrays(dtype="U99", shape=(10,))) def test_can_unicode_strings_without_decode_error(arr): # See https://github.com/numpy/numpy/issues/15363 pass @pytest.mark.skipif(not nps.NP_FIXED_UNICODE, reason="workaround for old bug") def test_unicode_string_dtypes_need_not_be_utf8(): def cannot_encode(string): try: string.encode() return False except UnicodeEncodeError: return True find_any(nps.from_dtype(np.dtype("U")), cannot_encode, settings(max_examples=5000)) @given(st.data()) def test_byte_string_dtypes_generate_unicode_strings(data): dt = data.draw(nps.byte_string_dtypes()) result = data.draw(nps.from_dtype(dt)) assert isinstance(result, bytes) skipif_np2 = pytest.mark.skipif(np_version >= (2, 0), reason="removed in new version") @pytest.mark.parametrize( "dtype", ["U", "S", pytest.param("a", marks=skipif_np2)], ) def test_unsized_strings_length_gt_one(dtype): # See https://github.com/HypothesisWorks/hypothesis/issues/2229 find_any(nps.arrays(dtype=dtype, shape=1), lambda arr: len(arr[0]) >= 2) @given( st.data(), st.builds( "{}[{}]".format, st.sampled_from(("datetime64", "timedelta64")), st.sampled_from(nps.TIME_RESOLUTIONS), ).map(np.dtype), ) def test_inferring_from_time_dtypes_gives_same_dtype(data, dtype): ex = data.draw(nps.from_dtype(dtype)) assert dtype == ex.dtype @given(st.data(), nps.byte_string_dtypes() | nps.unicode_string_dtypes()) def test_inferred_string_strategies_roundtrip(data, dtype): # Check that we never generate too-long or nul-terminated strings, which # cannot be read back out of an array. arr = np.zeros(shape=1, dtype=dtype) ex = data.draw(nps.from_dtype(arr.dtype)) arr[0] = ex assert arr[0] == ex @given(st.data(), nps.scalar_dtypes()) def test_all_inferred_scalar_strategies_roundtrip(data, dtype): # We only check scalars here, because record/compound/nested dtypes always # give an array of np.void objects. We're interested in whether scalar # values are safe, not known type coercion. arr = np.zeros(shape=1, dtype=dtype) ex = data.draw(nps.from_dtype(arr.dtype)) assume(ex == ex) # If not, the roundtrip test *should* fail! (eg NaN) arr[0] = ex assert arr[0] == ex @pytest.mark.parametrize("dtype_str", ["m8", "M8"]) @given(data=st.data()) def test_from_dtype_works_without_time_unit(data, dtype_str): arr = data.draw(nps.from_dtype(np.dtype(dtype_str))) assert (dtype_str + "[") in arr.dtype.str @pytest.mark.parametrize("dtype_str", ["m8", "M8"]) @given(data=st.data()) def test_arrays_selects_consistent_time_unit(data, dtype_str): arr = data.draw(nps.arrays(dtype_str, 10)) assert (dtype_str + "[") in arr.dtype.str @pytest.mark.parametrize("dtype", ["m8", "M8"]) def test_from_dtype_can_include_or_exclude_nat(dtype): find_any(nps.from_dtype(np.dtype(dtype), allow_nan=None), np.isnat) find_any(nps.from_dtype(np.dtype(dtype), allow_nan=True), np.isnat) assert_no_examples(nps.from_dtype(np.dtype(dtype), allow_nan=False), np.isnat) def test_arrays_gives_useful_error_on_inconsistent_time_unit(): with pytest.raises(InvalidArgument, match="mismatch of time units in dtypes"): check_can_generate_examples( nps.arrays("m8[Y]", 10, elements=nps.from_dtype(np.dtype("m8[D]"))) ) @pytest.mark.parametrize( "dtype, kwargs, pred", [ # Floating point: bounds, exclusive bounds, and excluding nonfinites (float, {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2), ( float, {"min_value": 1, "max_value": 2, "exclude_min": True, "exclude_max": True}, lambda x: 1 < x < 2, ), (float, {"allow_nan": False}, lambda x: not np.isnan(x)), (float, {"allow_infinity": False}, lambda x: not np.isinf(x)), (float, {"allow_nan": False, "allow_infinity": False}, np.isfinite), # Complex numbers: bounds and excluding nonfinites (complex, {"allow_nan": False}, lambda x: not np.isnan(x)), (complex, {"allow_infinity": False}, lambda x: not np.isinf(x)), (complex, {"allow_nan": False, "allow_infinity": False}, np.isfinite), # Note we accept epsilon errors here as internally sqrt is used to draw # complex numbers. sqrt on some platforms gets epsilon errors, which is # too tricky to filter out and so - for now - we just accept them. ( complex, {"min_magnitude": 1e3}, lambda x: abs(x) >= 1e3 * (1 - sys.float_info.epsilon), ), ( complex, {"max_magnitude": 1e2}, lambda x: abs(x) <= 1e2 * (1 + sys.float_info.epsilon), ), ( complex, {"min_magnitude": 1, "max_magnitude": 1e6}, lambda x: ( (1 - sys.float_info.epsilon) <= abs(x) <= 1e6 * (1 + sys.float_info.epsilon) ), ), # Integer bounds, limited to the representable range ("int8", {"min_value": -1, "max_value": 1}, lambda x: -1 <= x <= 1), ("uint8", {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2), # String arguments, bounding size and unicode alphabet ("S", {"min_size": 1, "max_size": 2}, lambda x: 1 <= len(x) <= 2), ("S4", {"min_size": 1, "max_size": 2}, lambda x: 1 <= len(x) <= 2), ("U", {"min_size": 1, "max_size": 2}, lambda x: 1 <= len(x) <= 2), ("U4", {"min_size": 1, "max_size": 2}, lambda x: 1 <= len(x) <= 2), ("U", {"alphabet": "abc"}, lambda x: set(x).issubset("abc")), ], ) @given(data=st.data()) def test_from_dtype_with_kwargs(data, dtype, kwargs, pred): value = data.draw(nps.from_dtype(np.dtype(dtype), **kwargs)) assert pred(value) @given(nps.from_dtype(np.dtype("U20,uint8,float32"), min_size=1, allow_nan=False)) def test_customize_structured_dtypes(x): name, age, score = x assert len(name) >= 1 assert 0 <= age <= 255 assert not np.isnan(score) @pytest.mark.parametrize("allow_subnormal", [False, True]) @pytest.mark.parametrize("width", [32, 64]) def test_float_subnormal_generation(allow_subnormal, width): dtype = np.dtype(f"float{width}") strat = nps.from_dtype(dtype, allow_subnormal=allow_subnormal).filter( lambda n: n != 0 ) smallest_normal = width_smallest_normals[width] if allow_subnormal: find_any(strat, lambda n: -smallest_normal < n < smallest_normal) else: assert_no_examples(strat, lambda n: -smallest_normal < n < smallest_normal) @pytest.mark.parametrize("allow_subnormal", [False, True]) @pytest.mark.parametrize("width", [64, 128]) def test_complex_subnormal_generation(allow_subnormal, width): dtype = np.dtype(f"complex{width}") strat = nps.from_dtype(dtype, allow_subnormal=allow_subnormal).filter( lambda n: n.real != 0 and n.imag != 0 ) smallest_normal = width_smallest_normals[width / 2] def condition(n): return ( -smallest_normal < n.real < smallest_normal or -smallest_normal < n.imag < smallest_normal ) if allow_subnormal: find_any(strat, condition) else: assert_no_examples(strat, condition) ================================================ FILE: hypothesis-python/tests/numpy/test_from_type.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import typing import warnings import numpy as np import pytest from hypothesis import given, strategies as st from hypothesis.errors import SmallSearchSpaceWarning from hypothesis.extra.numpy import ArrayLike, NDArray, _NestedSequence, _SupportsArray from .test_from_dtype import STANDARD_TYPES from tests.common.debug import assert_simple_property, find_any STANDARD_TYPES_TYPE = [dtype.type for dtype in STANDARD_TYPES] needs_np_typing = {"reason": "numpy.typing is not available"} needs_np_private_typing = {"reason": "numpy._typing is not available"} @given(dtype=st.from_type(np.dtype)) def test_resolves_dtype_type(dtype): assert isinstance(dtype, np.dtype) def test_does_not_resolve_nonscalar_types(): # this was previously a parametrized test over np.object_ and np.void which # used the same repr code path for the test. But then numpy changed their types # such that we defer evaluation for st.from_type and are no longer identical # to st.builds, but rather something morally equivalent to it. So we have # these slightly more complicated checks. with warnings.catch_warnings(): warnings.simplefilter("ignore", SmallSearchSpaceWarning) assert_simple_property(st.from_type(np.object_), lambda value: value is None) with pytest.raises(TypeError): # np.void() requires an argument, and so throws when instantiated assert_simple_property(st.from_type(np.void)) @pytest.mark.parametrize("typ", STANDARD_TYPES_TYPE) def test_resolves_and_varies_numpy_scalar_type(typ): # Check that we find an instance that is not equal to the default x = find_any(st.from_type(typ), lambda x: x != type(x)()) assert isinstance(x, typ) @pytest.mark.parametrize("atype", [np.ndarray, NDArray]) def test_resolves_unspecified_array_type(atype): if atype is not None: assert_simple_property(st.from_type(atype), lambda v: isinstance(v, np.ndarray)) def workaround(dtype): # Total hack to work around https://github.com/numpy/numpy/issues/24043 if np.__version__ == "1.25.0" and dtype == np.dtype("bytes").type: return pytest.param(dtype, marks=[pytest.mark.xfail(strict=False)]) return dtype # https://numpy.org/devdocs/release/1.22.0-notes.html#ndarray-dtype-and-number-are-now-runtime-subscriptable @pytest.mark.skipif( tuple(int(x) for x in np.__version__.split(".")[:2]) < (1, 22), reason="see comment" ) @pytest.mark.parametrize("typ", [workaround(t) for t in STANDARD_TYPES_TYPE]) def test_resolves_specified_ndarray_type(typ): assert_simple_property( st.from_type(np.ndarray[typ]), lambda arr: isinstance(arr, np.ndarray) and arr.dtype.type == typ, ) assert_simple_property( st.from_type(np.ndarray[typing.Any, typ]), lambda arr: isinstance(arr, np.ndarray) and arr.dtype.type == typ, ) @pytest.mark.skipif(NDArray is None, **needs_np_typing) @pytest.mark.parametrize("typ", [workaround(t) for t in STANDARD_TYPES_TYPE]) def test_resolves_specified_NDArray_type(typ): assert_simple_property( st.from_type(NDArray[typ]), lambda arr: isinstance(arr, np.ndarray) and arr.dtype.type == typ, ) @pytest.mark.skipif(NDArray is None, **needs_np_typing) def test_resolves_NDArray_with_dtype_union(): strat = st.from_type(NDArray[np.float64 | np.complex128]) find_any(strat, lambda arr: arr.dtype == np.dtype("float64")) find_any(strat, lambda arr: arr.dtype == np.dtype("complex128")) @pytest.mark.skipif(ArrayLike is None, **needs_np_typing) @given(arr_like=st.from_type(ArrayLike)) def test_resolves_ArrayLike_type(arr_like): arr = np.array(arr_like) assert isinstance(arr, np.ndarray) # The variation is too large to assert anything else about arr, but the # ArrayLike contract just says that it can be coerced into an array (which # we just did). @pytest.mark.skipif(_NestedSequence is None, **needs_np_private_typing) def test_resolves_specified_NestedSequence(): @given(seq=st.from_type(_NestedSequence[int])) def test(seq): assert hasattr(seq, "__iter__") def flatten(lst): for el in lst: try: yield from flatten(el) except TypeError: yield el assert all(isinstance(i, int) for i in flatten(seq)) test() @pytest.mark.skipif(_NestedSequence is None, **needs_np_private_typing) @given(seq=st.from_type(_NestedSequence)) def test_resolves_unspecified_NestedSequence(seq): assert hasattr(seq, "__iter__") @pytest.mark.skipif(_SupportsArray is None, **needs_np_private_typing) @given(arr=st.from_type(_SupportsArray)) def test_resolves_unspecified_SupportsArray(arr): assert hasattr(arr, "__array__") @pytest.mark.skipif(_SupportsArray is None, **needs_np_private_typing) def test_resolves_SupportsArray(): @given(arr=st.from_type(_SupportsArray[int])) def test(arr): assert hasattr(arr, "__array__") assert np.asarray(arr).dtype.kind == "i" test() @pytest.mark.skipif( _NestedSequence is None or _SupportsArray is None, **needs_np_private_typing ) def test_resolve_ArrayLike_equivalent(): # This is the current (1.24.3) definition of ArrayLike, # with problematic parts commented out. ArrayLike_like = ( _SupportsArray # | _NestedSequence[_SupportsArray], | bool | int | float | complex | str | bytes | _NestedSequence[ bool | int | float | complex | str, # | bytes, ] ) @given(arr_like=st.from_type(ArrayLike_like)) def test(arr_like): arr = np.array(arr_like) assert isinstance(arr, np.ndarray) test() ================================================ FILE: hypothesis-python/tests/numpy/test_gen_data.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys from functools import reduce from itertools import zip_longest import numpy as np import pytest from hypothesis import ( HealthCheck, Phase, assume, given, note, settings, strategies as st, target, ) from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption from hypothesis.extra import numpy as nps from hypothesis.strategies._internal.lazy import unwrap_strategies from tests.common.debug import ( assert_all_examples, check_can_generate_examples, find_any, minimal, ) from tests.common.utils import fails_with, flaky ANY_SHAPE = nps.array_shapes(min_dims=0, max_dims=32, min_side=0, max_side=32) ANY_NONZERO_SHAPE = nps.array_shapes(min_dims=0, max_dims=32, min_side=1, max_side=32) @given(nps.arrays(float, ())) def test_empty_dimensions_are_arrays(x): assert isinstance(x, np.ndarray) assert x.dtype.kind == "f" @given(nps.arrays(float, (1, 0, 1))) def test_can_handle_zero_dimensions(x): assert x.shape == (1, 0, 1) @given(nps.arrays("uint32", (5, 5))) def test_generates_unsigned_ints(x): assert (x >= 0).all() @given(nps.arrays(int, (1,))) def test_assert_fits_in_machine_size(x): pass def test_generates_and_minimizes(): assert (minimal(nps.arrays(float, (2, 2))) == np.zeros(shape=(2, 2))).all() def test_can_minimize_large_arrays(): x = minimal(nps.arrays("uint32", 100), lambda x: np.any(x) and not np.all(x)) assert np.logical_or(x == 0, x == 1).all() assert np.count_nonzero(x) in (1, len(x) - 1) @flaky(max_runs=50, min_passes=1) def test_can_minimize_float_arrays(): with np.errstate(over="ignore", invalid="ignore"): x = minimal(nps.arrays(float, 50), lambda t: np.nansum(t) >= 1.0) assert x.sum() in (1, 50) class Foo: pass foos = st.tuples().map(lambda _: Foo()) def test_can_create_arrays_of_composite_types(): arr = minimal(nps.arrays(object, 100, elements=foos)) for x in arr: assert isinstance(x, Foo) @given(st.lists(st.integers()), st.data()) def test_can_create_zero_dim_arrays_of_lists(x, data): arr = data.draw(nps.arrays(object, (), elements=st.just(x))) assert arr.shape == () assert arr.dtype == np.dtype(object) assert arr.item() == x def test_can_create_arrays_of_tuples(): arr = minimal( nps.arrays(object, 10, elements=st.tuples(st.integers(), st.integers())), lambda x: all(t0 != t1 for t0, t1 in x), ) assert all(a in ((1, 0), (0, 1)) for a in arr) @given(nps.arrays(object, (2, 2), elements=st.tuples(st.integers()))) def test_does_not_flatten_arrays_of_tuples(arr): assert isinstance(arr[0][0], tuple) @given( nps.arrays(object, (2, 2), elements=st.lists(st.integers(), min_size=1, max_size=1)) ) def test_does_not_flatten_arrays_of_lists(arr): assert isinstance(arr[0][0], list) @given(nps.array_shapes()) def test_can_generate_array_shapes(shape): assert isinstance(shape, tuple) assert all(isinstance(i, int) for i in shape) @settings( deadline=None, max_examples=10, suppress_health_check=[HealthCheck.nested_given] ) @given(st.integers(0, 10), st.integers(0, 9), st.integers(0), st.integers(0)) def test_minimise_array_shapes(min_dims, dim_range, min_side, side_range): smallest = minimal( nps.array_shapes( min_dims=min_dims, max_dims=min_dims + dim_range, min_side=min_side, max_side=min_side + side_range, ) ) assert len(smallest) == min_dims assert all(k == min_side for k in smallest) @pytest.mark.parametrize( "kwargs", [{"min_side": 100}, {"min_dims": 15}, {"min_dims": 32}] ) def test_interesting_array_shapes_argument(kwargs): check_can_generate_examples(nps.array_shapes(**kwargs)) @given(nps.scalar_dtypes()) def test_can_generate_scalar_dtypes(dtype): assert isinstance(dtype, np.dtype) @settings(max_examples=100) @given( nps.nested_dtypes( subtype_strategy=st.one_of( nps.scalar_dtypes(), nps.byte_string_dtypes(), nps.unicode_string_dtypes() ) ) ) def test_can_generate_compound_dtypes(dtype): assert isinstance(dtype, np.dtype) @settings(max_examples=100) @given( nps.nested_dtypes( subtype_strategy=st.one_of( nps.scalar_dtypes(), nps.byte_string_dtypes(), nps.unicode_string_dtypes() ) ).flatmap(lambda dt: nps.arrays(dtype=dt, shape=1)) ) def test_can_generate_data_compound_dtypes(arr): # This is meant to catch the class of errors which prompted PR #2085 assert isinstance(arr, np.ndarray) @given(nps.nested_dtypes()) def test_np_dtype_is_idempotent(dtype): assert dtype == np.dtype(dtype) def test_minimise_scalar_dtypes(): assert minimal(nps.scalar_dtypes()) == np.dtype("bool") def test_minimise_nested_types(): assert minimal(nps.nested_dtypes()) == np.dtype("bool") def test_minimise_array_strategy(): smallest = minimal( nps.arrays( nps.nested_dtypes(max_itemsize=200), nps.array_shapes(max_dims=3, max_side=3), ) ) assert smallest.dtype == np.dtype("bool") assert not smallest.any() @given(nps.array_dtypes(allow_subarrays=False)) def test_can_turn_off_subarrays(dt): for name in dt.names: assert dt.fields[name][0].shape == () def test_array_dtypes_may_have_field_titles(): find_any(nps.array_dtypes(), lambda dt: len(dt.fields) > len(dt.names)) @pytest.mark.parametrize("byteorder", ["<", ">"]) @given(data=st.data()) def test_can_restrict_endianness(data, byteorder): dtype = data.draw(nps.integer_dtypes(endianness=byteorder, sizes=(16, 32, 64))) if byteorder == ("<" if sys.byteorder == "little" else ">"): assert dtype.byteorder == "=" else: assert dtype.byteorder == byteorder @given(nps.integer_dtypes(sizes=8)) def test_can_specify_size_as_an_int(dt): assert dt.itemsize == 1 @given(st.data()) def test_can_draw_arrays_from_scalars(data): dt = data.draw(nps.scalar_dtypes()) result = data.draw(nps.arrays(dtype=dt, shape=())) assert isinstance(result, np.ndarray) assert result.dtype == dt @given(st.data()) def test_can_cast_for_arrays(data): # Note: this only passes with castable datatypes, certain dtype # combinations will result in an error if numpy is not able to cast them. dt_elements = np.dtype(data.draw(st.sampled_from(["bool", "i2"]))) dt_desired = np.dtype( data.draw(st.sampled_from(["i2", "float32", "float64"])) ) result = data.draw( nps.arrays( dtype=dt_desired, elements=nps.from_dtype(dt_elements), shape=(1, 2, 3) ) ) assert isinstance(result, np.ndarray) assert result.dtype == dt_desired @given(nps.arrays(dtype="int8", shape=st.integers(0, 20), unique=True)) def test_array_values_are_unique(arr): assert len(set(arr)) == len(arr) def test_cannot_generate_unique_array_of_too_many_elements(): strat = nps.arrays(dtype=int, elements=st.integers(0, 5), shape=10, unique=True) with pytest.raises(InvalidArgument): check_can_generate_examples(strat) @given( nps.arrays( elements=st.just(0.0), dtype=float, fill=st.just(np.nan), shape=st.integers(0, 20), unique=True, ) ) def test_array_values_are_unique_high_collision(arr): assert (arr == 0.0).sum() <= 1 @given(nps.arrays(dtype="int8", shape=(4,), elements=st.integers(0, 3), unique=True)) def test_generates_all_values_for_unique_array(arr): # Ensures that the "reject already-seen element" branch is covered assert len(set(arr)) == len(arr) @given(nps.arrays(dtype="int8", shape=255, unique=True)) def test_efficiently_generates_all_unique_array(arr): # Avoids the birthday paradox with UniqueSampledListStrategy assert len(set(arr)) == len(arr) @given(st.data(), st.integers(-100, 100), st.integers(1, 100)) def test_array_element_rewriting(data, start, size): arr = nps.arrays( dtype=np.dtype("int64"), shape=size, elements=st.integers(start, start + size - 1), unique=True, ) assert set(data.draw(arr)) == set(range(start, start + size)) def test_may_fill_with_nan_when_unique_is_set(): find_any( nps.arrays( dtype=float, elements=st.floats(allow_nan=False), shape=10, unique=True, fill=st.just(np.nan), ), lambda x: np.isnan(x).any(), ) @given( nps.arrays( dtype=float, elements=st.floats(allow_nan=False), shape=10, unique=True, fill=st.just(np.nan), ) ) def test_is_still_unique_with_nan_fill(xs): assert len(set(xs)) == len(xs) @fails_with(InvalidArgument) @given( nps.arrays( dtype=float, elements=st.floats(allow_nan=False), shape=10, unique=True, fill=st.just(0.0), ) ) def test_may_not_fill_with_non_nan_when_unique_is_set(arr): pass @fails_with(InvalidArgument) @given(nps.arrays(dtype="U", shape=10, unique=True, fill=st.just(""))) def test_may_not_fill_with_non_nan_when_unique_is_set_and_type_is_not_number(arr): pass np_version = tuple(int(x) for x in np.__version__.split(".")[:2]) @pytest.mark.parametrize("fill", [False, True]) # Overflowing elements deprecated upstream in Numpy 1.24 :-) @fails_with( InvalidArgument if np_version < (1, 24) else (DeprecationWarning if np_version < (2, 0) else OverflowError) ) @given(st.data()) def test_overflowing_integers_are_deprecated(fill, data): kw = {"elements": st.just(300)} if fill: kw = {"elements": st.nothing(), "fill": kw["elements"]} arr = data.draw(nps.arrays(dtype="int8", shape=(1,), **kw)) assert arr[0] == (300 % 256) @pytest.mark.parametrize("fill", [False, True]) @pytest.mark.parametrize( "dtype,strat", [ ("float16", st.floats(min_value=65520, allow_infinity=False)), ("float32", st.floats(min_value=10**40, allow_infinity=False)), ( "complex64", st.complex_numbers(min_magnitude=10**300, allow_infinity=False), ), ("U1", st.text(min_size=2, max_size=2)), ("S1", st.binary(min_size=2, max_size=2)), ], ) @fails_with(InvalidArgument) @given(data=st.data()) def test_unrepresentable_elements_are_deprecated(fill, dtype, strat, data): if fill: kw = {"elements": st.nothing(), "fill": strat} else: kw = {"elements": strat} try: arr = data.draw(nps.arrays(dtype=dtype, shape=(1,), **kw)) except RuntimeWarning: assert np_version >= (1, 24), "New overflow-on-cast detection" raise InvalidArgument("so the test passes") from None try: # This is a float or complex number, and has overflowed to infinity, # triggering our deprecation for overflow. assert np.isinf(arr[0]) except TypeError: # We tried to call isinf on a string. The string was generated at # length two, then truncated by the dtype of size 1 - deprecation # again. If the first character was \0 it is now the empty string. assert len(arr[0]) <= 1 @given(nps.arrays(dtype="float16", shape=(1,))) def test_inferred_floats_do_not_overflow(arr): pass @given(nps.arrays(dtype="float16", shape=10, elements={"min_value": 0, "max_value": 1})) def test_inferred_floats_can_be_constrained_at_low_width(arr): assert (arr >= 0).all() assert (arr <= 1).all() @given( nps.arrays( dtype="float16", shape=10, elements={ "min_value": 0, "max_value": 1, "exclude_min": True, "exclude_max": True, }, ) ) def test_inferred_floats_can_be_constrained_at_low_width_excluding_endpoints(arr): assert (arr > 0).all() assert (arr < 1).all() @given( nps.arrays( dtype="float16", shape=10, unique=True, elements=st.integers(1, 9), fill=st.just(np.nan), ) ) def test_unique_array_with_fill_can_use_all_elements(arr): assume(len(set(arr)) == arr.size) @given(nps.arrays(dtype="uint8", shape=25, unique=True, fill=st.nothing())) def test_unique_array_without_fill(arr): # This test covers the collision-related branches for fully dense unique arrays. # Choosing 25 of 256 possible elements means we're almost certain to see collisions # thanks to the 'birthday paradox', but finding unique elemennts is still easy. assume(len(set(arr)) == arr.size) @given(ndim=st.integers(0, 5), data=st.data()) def test_mapped_positive_axes_are_unique(ndim, data): min_size = data.draw(st.integers(0, ndim), label="min_size") max_size = data.draw(st.integers(min_size, ndim), label="max_size") axes = data.draw( nps.valid_tuple_axes(ndim, min_size=min_size, max_size=max_size), label="axes" ) assert len(set(axes)) == len({i if 0 < i else ndim + i for i in axes}) @given(ndim=st.integers(0, 5), data=st.data()) def test_length_bounds_are_satisfied(ndim, data): min_size = data.draw(st.integers(0, ndim), label="min_size") max_size = data.draw(st.integers(min_size, ndim), label="max_size") axes = data.draw( nps.valid_tuple_axes(ndim, min_size=min_size, max_size=max_size), label="axes" ) assert min_size <= len(axes) <= max_size @given(shape=nps.array_shapes(), data=st.data()) def test_axes_are_valid_inputs_to_sum(shape, data): x = np.zeros(shape, dtype="uint8") axes = data.draw(nps.valid_tuple_axes(ndim=len(shape)), label="axes") np.sum(x, axes) @settings( deadline=None, max_examples=10, suppress_health_check=[HealthCheck.nested_given] ) @given(ndim=st.integers(0, 3), data=st.data()) def test_minimize_tuple_axes(ndim, data): min_size = data.draw(st.integers(0, ndim), label="min_size") max_size = data.draw(st.integers(min_size, ndim), label="max_size") smallest = minimal(nps.valid_tuple_axes(ndim, min_size=min_size, max_size=max_size)) assert len(smallest) == min_size assert all(k > -1 for k in smallest) @settings( deadline=None, max_examples=10, suppress_health_check=[HealthCheck.nested_given] ) @given(ndim=st.integers(0, 3), data=st.data()) def test_minimize_negative_tuple_axes(ndim, data): min_size = data.draw(st.integers(0, ndim), label="min_size") max_size = data.draw(st.integers(min_size, ndim), label="max_size") smallest = minimal( nps.valid_tuple_axes(ndim, min_size=min_size, max_size=max_size), lambda x: all(i < 0 for i in x), ) assert len(smallest) == min_size @given(nps.broadcastable_shapes((), min_side=0, max_side=0, min_dims=0, max_dims=0)) def test_broadcastable_empty_shape(shape): assert shape == () @settings(deadline=None, suppress_health_check=[HealthCheck.too_slow]) @given(shape=ANY_SHAPE, data=st.data()) def test_broadcastable_shape_bounds_are_satisfied(shape, data): min_dims = data.draw(st.integers(0, 32), label="min_dims") max_dims = data.draw(st.none() | st.integers(min_dims, 32), label="max_dims") min_side = data.draw(st.integers(0, 3), label="min_side") max_side = data.draw(st.none() | st.integers(min_side, 6), label="max_side") try: bshape = data.draw( nps.broadcastable_shapes( shape, min_side=min_side, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ), label="bshape", ) except InvalidArgument: raise UnsatisfiedAssumption from None if max_dims is None: max_dims = max(len(shape), min_dims) + 2 if max_side is None: max_side = max((*shape[::-1][:max_dims], min_side)) + 2 assert isinstance(bshape, tuple) assert all(isinstance(s, int) for s in bshape) assert min_dims <= len(bshape) <= max_dims assert all(min_side <= s <= max_side for s in bshape) @settings(deadline=None) @given(num_shapes=st.integers(1, 4), base_shape=ANY_SHAPE, data=st.data()) def test_mutually_broadcastable_shape_bounds_are_satisfied( num_shapes, base_shape, data ): min_dims = data.draw(st.integers(0, 32), label="min_dims") max_dims = data.draw( st.one_of(st.none(), st.integers(min_dims, 32)), label="max_dims" ) min_side = data.draw(st.integers(0, 3), label="min_side") max_side = data.draw( st.one_of(st.none(), st.integers(min_side, 6)), label="max_side" ) try: shapes, result = data.draw( nps.mutually_broadcastable_shapes( num_shapes=num_shapes, base_shape=base_shape, min_side=min_side, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ), label="shapes, result", ) except InvalidArgument: raise UnsatisfiedAssumption from None if max_dims is None: max_dims = max(len(base_shape), min_dims) + 2 if max_side is None: max_side = max((*base_shape[::-1][:max_dims], min_side)) + 2 assert isinstance(shapes, tuple) assert isinstance(result, tuple) assert all(isinstance(s, int) for s in result) for bshape in shapes: assert isinstance(bshape, tuple) assert all(isinstance(s, int) for s in bshape) assert min_dims <= len(bshape) <= max_dims assert all(min_side <= s <= max_side for s in bshape) def _draw_valid_bounds(data, shape, max_dims, *, permit_none=True): if max_dims == 0 or not shape: return 0, None smallest_side = min(shape[::-1][:max_dims]) min_strat = ( st.sampled_from([1, smallest_side]) if smallest_side > 1 else st.just(smallest_side) ) min_side = data.draw(min_strat, label="min_side") largest_side = max(max(shape[::-1][:max_dims]), min_side) if permit_none: max_strat = st.one_of(st.none(), st.integers(largest_side, largest_side + 2)) else: max_strat = st.integers(largest_side, largest_side + 2) max_side = data.draw(max_strat, label="max_side") return min_side, max_side def _broadcast_two_shapes(shape_a: nps.Shape, shape_b: nps.Shape) -> nps.Shape: result = [] for a, b in zip_longest(reversed(shape_a), reversed(shape_b), fillvalue=1): if a != b and (a != 1) and (b != 1): raise ValueError( f"shapes {shape_a!r} and {shape_b!r} are not broadcast-compatible" ) result.append(a if a != 1 else b) return tuple(reversed(result)) def _broadcast_shapes(*shapes): """Returns the shape resulting from broadcasting the input shapes together. Raises ValueError if the shapes are not broadcast-compatible""" assert shapes, "Must pass >=1 shapes to broadcast" return reduce(_broadcast_two_shapes, shapes, ()) @settings(deadline=None, max_examples=500) @given( shapes=st.lists( nps.array_shapes(min_dims=0, min_side=0, max_dims=4, max_side=4), min_size=1 ) ) def test_broadcastable_shape_util(shapes): """Ensures that `_broadcast_shapes` raises when fed incompatible shapes, and ensures that it produces the true broadcasted shape""" if len(shapes) == 1: assert _broadcast_shapes(*shapes) == shapes[0] return arrs = [np.zeros(s, dtype=np.uint8) for s in shapes] try: broadcast_out = np.broadcast_arrays(*arrs) except ValueError: with pytest.raises(ValueError): _broadcast_shapes(*shapes) return broadcasted_shape = _broadcast_shapes(*shapes) assert broadcast_out[0].shape == broadcasted_shape @settings(deadline=None, max_examples=200) @given(shape=ANY_NONZERO_SHAPE, data=st.data()) def test_broadcastable_shape_has_good_default_values(shape, data): # This test ensures that default parameters can always produce broadcast-compatible shapes broadcastable_shape = data.draw( nps.broadcastable_shapes(shape), label="broadcastable_shapes" ) # error if drawn shape for b is not broadcast-compatible _broadcast_shapes(shape, broadcastable_shape) @settings(deadline=None, max_examples=200) @given(base_shape=ANY_SHAPE, num_shapes=st.integers(1, 10), data=st.data()) def test_mutually_broadcastableshapes_has_good_default_values( num_shapes, base_shape, data ): # This test ensures that default parameters can always produce broadcast-compatible shapes shapes, result = data.draw( nps.mutually_broadcastable_shapes(num_shapes=num_shapes, base_shape=base_shape), label="shapes, result", ) assert len(shapes) == num_shapes # raises if shapes are not mutually-compatible assert result == _broadcast_shapes(base_shape, *shapes) @settings(deadline=None) @given(min_dims=st.integers(0, 32), shape=ANY_SHAPE, data=st.data()) def test_broadcastable_shape_can_broadcast(min_dims, shape, data): max_dims = data.draw(st.none() | st.integers(min_dims, 32), label="max_dims") min_side, max_side = _draw_valid_bounds(data, shape, max_dims) broadcastable_shape = data.draw( nps.broadcastable_shapes( shape, min_side=min_side, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ), label="broadcastable_shapes", ) # error if drawn shape for b is not broadcast-compatible _broadcast_shapes(shape, broadcastable_shape) @settings(deadline=None) @given( num_shapes=st.integers(1, 10), min_dims=st.integers(0, 32), base_shape=ANY_SHAPE, data=st.data(), ) def test_mutually_broadcastable_shape_can_broadcast( num_shapes, min_dims, base_shape, data ): max_dims = data.draw(st.none() | st.integers(min_dims, 32), label="max_dims") min_side, max_side = _draw_valid_bounds(data, base_shape, max_dims) shapes, result = data.draw( nps.mutually_broadcastable_shapes( num_shapes=num_shapes, base_shape=base_shape, min_side=min_side, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ), label="shapes, result", ) # error if drawn shapes are not mutually broadcast-compatible assert result == _broadcast_shapes(base_shape, *shapes) @settings( deadline=None, max_examples=50, suppress_health_check=[HealthCheck.nested_given] ) @given( num_shapes=st.integers(1, 3), min_dims=st.integers(0, 5), base_shape=nps.array_shapes(min_dims=0, max_dims=3, min_side=0, max_side=5), data=st.data(), ) def test_minimize_mutually_broadcastable_shape(num_shapes, min_dims, base_shape, data): # Ensure aligned dimensions of broadcastable shape minimizes to `(1,) * min_dims` max_dims = data.draw(st.none() | st.integers(min_dims, 5), label="max_dims") min_side, max_side = _draw_valid_bounds( data, base_shape, max_dims, permit_none=False ) if num_shapes > 1: # shrinking gets a little bit hairy when we have empty axes # and multiple num_shapes assume(min_side > 0) smallest_shapes, result = minimal( nps.mutually_broadcastable_shapes( num_shapes=num_shapes, base_shape=base_shape, min_side=min_side, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ) ) note(f"smallest_shapes: {smallest_shapes}") note(f"result: {result}") assert len(smallest_shapes) == num_shapes assert result == _broadcast_shapes(base_shape, *smallest_shapes) for smallest in smallest_shapes: n_leading = max(len(smallest) - len(base_shape), 0) n_aligned = max(len(smallest) - n_leading, 0) note(f"n_leading: {n_leading}") note(f"n_aligned: {n_aligned} {base_shape[-n_aligned:]}") expected = [min_side] * n_leading + [ (min(1, i) if i != 1 else min_side) if min_side <= 1 <= max_side else i for i in (base_shape[-n_aligned:] if n_aligned else ()) ] assert tuple(expected) == smallest @settings(deadline=None) @given(max_dims=st.integers(4, 6), data=st.data()) def test_broadcastable_shape_adjusts_max_dim_with_explicit_bounds(max_dims, data): # Ensures that `broadcastable_shapes` limits itself to satisfiable dimensions # Broadcastable values can only be drawn for dims 0-3 for these shapes shape = data.draw(st.sampled_from([(5, 3, 2, 1), (0, 3, 2, 1)]), label="shape") broadcastable_shape = data.draw( nps.broadcastable_shapes( shape, min_side=2, max_side=3, min_dims=3, max_dims=max_dims ), label="broadcastable_shapes", ) assert len(broadcastable_shape) == 3 # error if drawn shape for b is not broadcast-compatible _broadcast_shapes(shape, broadcastable_shape) @settings(deadline=None) @given( max_side=st.sampled_from([3, None]), min_dims=st.integers(0, 4), num_shapes=st.integers(1, 3), data=st.data(), ) def test_mutually_broadcastable_shape_adjusts_max_dim_with_default_bounds( max_side, min_dims, num_shapes, data ): # Ensures that `mutually_broadcastable_shapes` limits itself to satisfiable dimensions # when a default `max_dims` is derived. base_shape = data.draw( st.sampled_from([(5, 3, 2, 1), (0, 3, 2, 1)]), label="base_shape" ) try: shapes, result = data.draw( nps.mutually_broadcastable_shapes( num_shapes=num_shapes, base_shape=base_shape, min_side=2, max_side=max_side, min_dims=min_dims, ), label="shapes, result", ) except InvalidArgument: # There is no satisfiable `max_dims` for us to tune assert min_dims == 4 assert max_side == 3 or base_shape[0] == 0 return if max_side == 3 or base_shape[0] == 0: assert all(len(s) <= 3 for s in shapes) elif min_dims == 4: assert all(4 <= len(s) for s in shapes) # error if drawn shape for b is not broadcast-compatible assert len(shapes) == num_shapes assert result == _broadcast_shapes(base_shape, *shapes) @settings( deadline=None, max_examples=10, suppress_health_check=[HealthCheck.nested_given] ) @given(min_dims=st.integers(0, 32), min_side=st.integers(2, 3), data=st.data()) def test_broadcastable_shape_shrinking_with_singleton_out_of_bounds( min_dims, min_side, data ): max_dims = data.draw(st.none() | st.integers(min_dims, 32), label="max_dims") max_side = data.draw(st.none() | st.integers(min_side, 6), label="max_side") shape = data.draw(st.integers(1, 4).map(lambda n: n * (1,)), label="shape") smallest = minimal( nps.broadcastable_shapes( shape, min_side=min_side, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ) ) assert smallest == (min_side,) * min_dims @settings( deadline=None, max_examples=50, suppress_health_check=[HealthCheck.nested_given] ) @given( num_shapes=st.integers(1, 4), min_dims=st.integers(0, 4), min_side=st.integers(2, 3), data=st.data(), ) def test_mutually_broadcastable_shapes_shrinking_with_singleton_out_of_bounds( num_shapes, min_dims, min_side, data ): """Ensures that shapes minimize to `(min_side,) * min_dims` when singleton dimensions are disallowed.""" max_dims = data.draw(st.none() | st.integers(min_dims, 4), label="max_dims") max_side = data.draw( st.one_of(st.none(), st.integers(min_side, 6)), label="max_side" ) ndims = data.draw(st.integers(1, 4), label="ndim") base_shape = (1,) * ndims smallest_shapes, result = minimal( nps.mutually_broadcastable_shapes( num_shapes=num_shapes, base_shape=base_shape, min_side=min_side, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ) ) note(f"(smallest_shapes, result): {(smallest_shapes, result)}") assert len(smallest_shapes) == num_shapes assert result == _broadcast_shapes(base_shape, *smallest_shapes) for smallest in smallest_shapes: assert smallest == (min_side,) * min_dims @settings(suppress_health_check=[HealthCheck.too_slow]) @given( num_shapes=st.integers(1, 4), min_dims=st.integers(1, 32), max_side=st.integers(1, 6), data=st.data(), ) def test_mutually_broadcastable_shapes_only_singleton_is_valid( num_shapes, min_dims, max_side, data ): """Ensures that, when all aligned base-shape dim sizes are larger than ``max_side``, only singletons can be drawn""" max_dims = data.draw(st.integers(min_dims, 32), label="max_dims") base_shape = data.draw( nps.array_shapes(min_side=max_side + 1, min_dims=1), label="base_shape" ) input_shapes, result = data.draw( nps.mutually_broadcastable_shapes( num_shapes=num_shapes, base_shape=base_shape, min_side=1, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ), label="input_shapes, result", ) assert len(input_shapes) == num_shapes assert result == _broadcast_shapes(base_shape, *input_shapes) for shape in input_shapes: assert all(i == 1 for i in shape[-len(base_shape) :]) @settings(deadline=None, suppress_health_check=[HealthCheck.too_slow]) @given( shape=nps.array_shapes(min_dims=0, max_dims=3, min_side=0, max_side=5), max_dims=st.integers(0, 6), data=st.data(), ) def test_broadcastable_shape_can_generate_arbitrary_ndims(shape, max_dims, data): # ensures that generates shapes can possess any length in [min_dims, max_dims] desired_ndim = data.draw(st.integers(0, max_dims), label="desired_ndim") min_dims = data.draw( st.one_of(st.none(), st.integers(0, desired_ndim)), label="min_dims" ) # check default arg behavior too kwargs = {"min_dims": min_dims} if min_dims is not None else {} find_any( nps.broadcastable_shapes(shape, min_side=0, max_dims=max_dims, **kwargs), lambda x: len(x) == desired_ndim, settings(max_examples=10**6), ) @settings(deadline=None) @given( num_shapes=st.integers(1, 3), base_shape=nps.array_shapes(min_dims=0, max_dims=3, min_side=0, max_side=5), max_dims=st.integers(0, 4), data=st.data(), ) def test_mutually_broadcastable_shapes_can_generate_arbitrary_ndims( num_shapes, base_shape, max_dims, data ): # ensures that each generated shape can possess any length in [min_dims, max_dims] desired_ndims = data.draw( st.lists(st.integers(0, max_dims), min_size=num_shapes, max_size=num_shapes), label="desired_ndims", ) min_dims = data.draw( st.one_of(st.none(), st.integers(0, min(desired_ndims))), label="min_dims" ) # check default arg behavior too kwargs = {"min_dims": min_dims} if min_dims is not None else {} find_any( nps.mutually_broadcastable_shapes( num_shapes=num_shapes, base_shape=base_shape, min_side=0, max_dims=max_dims, **kwargs, ), lambda x: {len(s) for s in x.input_shapes} == set(desired_ndims), settings(max_examples=10**6), ) @settings(deadline=None, suppress_health_check=list(HealthCheck)) @given( base_shape=nps.array_shapes(min_dims=0, max_dims=3, min_side=0, max_side=2), max_dims=st.integers(1, 4), ) def test_mutually_broadcastable_shapes_can_generate_interesting_singletons( base_shape, max_dims ): find_any( nps.mutually_broadcastable_shapes( num_shapes=2, base_shape=base_shape, min_side=0, max_dims=max_dims, ), lambda x: any(a != b for a, b in zip(*(s[::-1] for s in x.input_shapes), strict=False)), # type: ignore ) @pytest.mark.parametrize("base_shape", [(), (0,), (1,), (2,), (1, 2), (2, 1), (2, 2)]) def test_mutually_broadcastable_shapes_can_generate_mirrored_singletons(base_shape): def f(shapes: nps.BroadcastableShapes): x, y = shapes.input_shapes return x.count(1) == 1 and y.count(1) == 1 and x[::-1] == y find_any( nps.mutually_broadcastable_shapes( num_shapes=2, base_shape=base_shape, min_side=0, max_side=3, min_dims=2, max_dims=2, ), f, ) @settings(deadline=None) @given( shape=nps.array_shapes(min_dims=1, min_side=1), dtype=st.one_of(nps.unsigned_integer_dtypes(), nps.integer_dtypes()), data=st.data(), ) def test_advanced_integer_index_is_valid_with_default_result_shape(shape, dtype, data): index = data.draw(nps.integer_array_indices(shape, dtype=dtype)) x = np.zeros(shape) out = x[index] # raises if the index is invalid assert not np.shares_memory(x, out) # advanced indexing should not return a view assert all(dtype == x.dtype for x in index) @settings(deadline=None) @given( shape=nps.array_shapes(min_dims=1, min_side=1), min_dims=st.integers(0, 3), min_side=st.integers(0, 3), dtype=st.one_of(nps.unsigned_integer_dtypes(), nps.integer_dtypes()), data=st.data(), ) def test_advanced_integer_index_is_valid_and_satisfies_bounds( shape, min_dims, min_side, dtype, data ): max_side = data.draw(st.integers(min_side, min_side + 2), label="max_side") max_dims = data.draw(st.integers(min_dims, min_dims + 2), label="max_dims") index = data.draw( nps.integer_array_indices( shape, result_shape=nps.array_shapes( min_dims=min_dims, max_dims=max_dims, min_side=min_side, max_side=max_side, ), dtype=dtype, ) ) x = np.zeros(shape) out = x[index] # raises if the index is invalid assert all(min_side <= s <= max_side for s in out.shape) assert min_dims <= out.ndim <= max_dims assert not np.shares_memory(x, out) # advanced indexing should not return a view assert all(dtype == x.dtype for x in index) @settings(deadline=None, suppress_health_check=[HealthCheck.nested_given]) @given( shape=nps.array_shapes(min_dims=1, min_side=1), min_dims=st.integers(0, 3), min_side=st.integers(0, 3), dtype=st.sampled_from(["uint8", "int8"]), data=st.data(), ) def test_advanced_integer_index_minimizes_as_documented( shape, min_dims, min_side, dtype, data ): max_side = data.draw(st.integers(min_side, min_side + 2), label="max_side") max_dims = data.draw(st.integers(min_dims, min_dims + 2), label="max_dims") result_shape = nps.array_shapes( min_dims=min_dims, max_dims=max_dims, min_side=min_side, max_side=max_side ) smallest = minimal( nps.integer_array_indices(shape, result_shape=result_shape, dtype=dtype) ) desired = len(shape) * (np.zeros(min_dims * [min_side]),) assert len(smallest) == len(desired) for s, d in zip(smallest, desired, strict=True): np.testing.assert_array_equal(s, d) @settings( deadline=None, max_examples=10, suppress_health_check=[HealthCheck.nested_given] ) @given( shape=nps.array_shapes(min_dims=1, max_dims=2, min_side=1, max_side=3), data=st.data(), ) def test_advanced_integer_index_can_generate_any_pattern(shape, data): # ensures that generated index-arrays can be used to yield any pattern of elements from an array x = np.arange(np.prod(shape)).reshape(shape) target_array = data.draw( nps.arrays( shape=nps.array_shapes(min_dims=1, max_dims=2, min_side=1, max_side=2), elements=st.sampled_from(x.flatten()), dtype=x.dtype, ), label="target", ) def index_selects_values_in_order(index): selected = x[index] target(len(set(selected.flatten())), label="unique indices") target(float(np.sum(target_array == selected)), label="elements correct") return np.all(target_array == selected) minimal( nps.integer_array_indices(shape, result_shape=st.just(target_array.shape)), index_selects_values_in_order, settings(max_examples=10**6, phases=[Phase.generate, Phase.target]), ) @pytest.mark.parametrize( "condition", [ lambda ix: isinstance(ix, tuple) and Ellipsis in ix, lambda ix: isinstance(ix, tuple) and Ellipsis not in ix, lambda ix: isinstance(ix, tuple) and np.newaxis in ix, lambda ix: isinstance(ix, tuple) and np.newaxis not in ix, lambda ix: ix is Ellipsis, lambda ix: ix == np.newaxis, ], ) def test_basic_indices_options(condition): indexers = nps.array_shapes(min_dims=0, max_dims=32).flatmap( lambda shape: nps.basic_indices(shape, allow_newaxis=True) ) find_any(indexers, condition) def test_basic_indices_can_generate_empty_tuple(): find_any(nps.basic_indices(shape=(0, 0), allow_ellipsis=True), lambda ix: ix == ()) def test_basic_indices_can_generate_non_tuples(): find_any( nps.basic_indices(shape=(0, 0), allow_ellipsis=True), lambda ix: not isinstance(ix, tuple), ) def test_basic_indices_can_generate_long_ellipsis(): # Runs of slice(None) - such as [0,:,:,:,0] - can be replaced by e.g. [0,...,0] find_any( nps.basic_indices(shape=(1, 0, 0, 0, 1), allow_ellipsis=True), lambda ix: len(ix) == 3 and ix[1] == Ellipsis, ) @given( nps.basic_indices(shape=(0, 0, 0, 0, 0)).filter( lambda idx: isinstance(idx, tuple) and Ellipsis in idx ) ) def test_basic_indices_replaces_whole_axis_slices_with_ellipsis(idx): # `slice(None)` (aka `:`) is the only valid index for an axis of size # zero, so if all dimensions are 0 then a `...` will replace all the # slices because we generate `...` for entire contiguous runs of `:` assert slice(None) not in idx def test_basic_indices_can_generate_indices_not_covering_all_dims(): # These "flat indices" are skippable in the underlying BasicIndexStrategy, # so we ensure we're definitely generating them for nps.basic_indices(). find_any( nps.basic_indices(shape=(3, 3, 3)), lambda ix: ( (not isinstance(ix, tuple) and ix != Ellipsis) or (isinstance(ix, tuple) and Ellipsis not in ix and len(ix) < 3) ), settings=settings(max_examples=5_000), ) @given( shape=nps.array_shapes(min_dims=0, max_side=4) | nps.array_shapes(min_dims=0, min_side=0, max_side=10), allow_newaxis=st.booleans(), allow_ellipsis=st.booleans(), data=st.data(), ) def test_basic_indices_generate_valid_indexers( shape, allow_newaxis, allow_ellipsis, data ): min_dims = data.draw( st.integers(0, 5 if allow_newaxis else len(shape)), label="min_dims" ) max_dims = data.draw( st.none() | st.integers(min_dims, 32 if allow_newaxis else len(shape)), label="max_dims", ) indexer = data.draw( nps.basic_indices( shape, min_dims=min_dims, max_dims=max_dims, allow_ellipsis=allow_ellipsis, allow_newaxis=allow_newaxis, ), label="indexer", ) # Check that disallowed things are indeed absent if not allow_newaxis: if isinstance(indexer, tuple): assert 0 <= len(indexer) <= len(shape) + int(allow_ellipsis) else: assert 1 <= len(shape) + int(allow_ellipsis) assert np.newaxis not in shape if not allow_ellipsis: assert Ellipsis not in shape if 0 in shape: # If there's a zero in the shape, the array will have no elements. array = np.zeros(shape) assert array.size == 0 elif np.prod(shape) <= 10**5: # If it's small enough to instantiate, do so with distinct elements. array = np.arange(np.prod(shape)).reshape(shape) else: # We can't cheat on this one, so just try another. assume(False) view = array[indexer] if not np.isscalar(view): assert min_dims <= view.ndim <= (32 if max_dims is None else max_dims) if view.size: assert np.shares_memory(view, array) # addresses https://github.com/HypothesisWorks/hypothesis/issues/2582 @given( nps.arrays( shape=nps.array_shapes(min_dims=0, min_side=0), dtype=nps.floating_dtypes() ) ) def test_array_owns_memory(x: np.ndarray): assert x.base is None assert x[...].base is x @given(st.data()) def test_no_recursion_in_multi_line_reprs_issue_3560(data): data.draw(nps.arrays(shape=(2,), dtype=float).map(lambda x: x)) data.draw( nps.arrays( shape=(2,), dtype=float, ).map(lambda x: x) ) def test_infers_elements_and_fill(): # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/3900 # We only infer a fill strategy if the elements_strategy has reusable values, # and the interaction of two performance fixes broke this. Oops... s = unwrap_strategies(nps.arrays(dtype=np.uint32, shape=1)) assert isinstance(s, nps.ArrayStrategy) assert repr(s.element_strategy) == f"integers(0, {2**32-1})" assert repr(s.fill) == f"integers(0, {2**32-1})" # But we _don't_ infer a fill if the elements strategy is non-reusable elems = st.builds(lambda x: x * 2, st.integers(1, 10)).map(np.uint32) assert not elems.has_reusable_values s = unwrap_strategies(nps.arrays(dtype=np.uint32, shape=1, elements=elems)) assert s.fill.is_empty @given(nps.arrays(np.dtype("O"), shape=nps.array_shapes())) def test_object_arrays_are_of_type_object(obj_array): assert obj_array.dtype == np.dtype("O") def test_class_instances_not_allowed_in_scalar_array(): class A: pass s = nps.arrays( nps.scalar_dtypes(), shape=nps.array_shapes(), elements=st.just(A()), ) # can raise ValueError during generation. For example if scalar_dtype is # corresponds to a datetime, numpy will raise "cannot convert A to a datetime". with pytest.raises((InvalidArgument, ValueError)): check_can_generate_examples(s) def test_object_arrays_with_mixed_elements_has_object_dtype(): class A: pass s = nps.arrays( np.dtype("O"), shape=nps.array_shapes(), elements=st.just(A()) | st.integers(), ) assert_all_examples(s, lambda arr: arr.dtype == np.dtype("O")) find_any(s, lambda arr: len({type(x) for x in arr.ravel()}) > 1) @given(st.data()) def test_object_array_can_hold_arbitrary_class_instances(data): instance = data.draw(st.from_type(type).flatmap(st.from_type)) s = nps.arrays(np.dtype("O"), nps.array_shapes(), elements=st.just(instance)) arr = data.draw(s) assert all(v is instance for v in arr.ravel()) def test_object_array_can_hold_incomparable_elements(): class Incomparable: def __eq__(self, other): raise TypeError check_can_generate_examples( nps.arrays( np.dtype("O"), nps.array_shapes(), elements=st.just(Incomparable()), ) ) def test_can_generate_nested_object_arrays(): int_arrays = nps.arrays(np.dtype("int"), nps.array_shapes()) s = nps.arrays(np.dtype("O"), nps.array_shapes(), elements=int_arrays) check_can_generate_examples(s) ================================================ FILE: hypothesis-python/tests/numpy/test_gufunc.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import numpy as np import pytest from pytest import param from hypothesis import HealthCheck, example, given, note, settings, strategies as st from hypothesis.extra import numpy as nps from hypothesis.extra._array_helpers import _hypothesis_parse_gufunc_signature from tests.common.debug import find_any, minimal def use_signature_examples(func): for sig in [ "(),()->()", "(i)->()", "(i),(i)->()", "(m,n),(n,p)->(m,p)", "(n),(n,p)->(p)", "(m,n),(n)->(m)", "(m?,n),(n,p?)->(m?,p?)", "(3),(3)->(3)", ]: func = example(sig)(func) return func def hy_sig_2_np_sig(hy_sig): return ( [tuple(d.strip("?") for d in shape) for shape in hy_sig.input_shapes], [tuple(d.strip("?") for d in hy_sig.result_shape)], ) def test_frozen_dims_signature(): _hypothesis_parse_gufunc_signature("(2),(3)->(4)") @st.composite def gufunc_arrays(draw, shape_strat, **kwargs): """An example user strategy built on top of mutually_broadcastable_shapes.""" input_shapes, result_shape = draw(shape_strat) arrays_strat = st.tuples(*(nps.arrays(shape=s, **kwargs) for s in input_shapes)) return draw(arrays_strat), result_shape @given( gufunc_arrays( nps.mutually_broadcastable_shapes(signature=np.matmul.signature), dtype="float64", elements=st.floats(0, 1000), ) ) def test_matmul_gufunc_shapes(everything): arrays, result_shape = everything out = np.matmul(*arrays) assert out.shape == result_shape @settings(deadline=None, max_examples=10, suppress_health_check=list(HealthCheck)) @pytest.mark.parametrize( "target_sig", ("(i),(i)->()", "(m,n),(n,p)->(m,p)", "(n),(n,p)->(p)", "(m,n),(n)->(m)"), ) @given(data=st.data()) def test_matmul_signature_can_exercise_all_combination_of_optional_dims( target_sig, data ): target_shapes = data.draw( nps.mutually_broadcastable_shapes(signature=target_sig, max_dims=0) ) find_any( nps.mutually_broadcastable_shapes( signature="(m?,n),(n,p?)->(m?,p?)", max_dims=0 ), lambda shapes: shapes == target_shapes, ) @settings( deadline=None, max_examples=50, suppress_health_check=[HealthCheck.nested_given] ) @given( min_dims=st.integers(0, 4), min_side=st.integers(2, 3), n_fixed=st.booleans(), data=st.data(), ) def test_matmul_sig_shrinks_as_documented(min_dims, min_side, n_fixed, data): sig = "(m?,n),(n,p?)->(m?,p?)" if n_fixed: n_value = data.draw(st.integers(0, 4)) sig = sig.replace("n", str(n_value)) else: n_value = min_side note(f"signature: {sig}") max_dims = data.draw(st.none() | st.integers(min_dims, 4), label="max_dims") max_side = data.draw(st.none() | st.integers(min_side, 6), label="max_side") smallest_shapes, result = minimal( nps.mutually_broadcastable_shapes( signature=sig, min_side=min_side, max_side=max_side, min_dims=min_dims, max_dims=max_dims, ) ) note(f"(smallest_shapes, result): {(smallest_shapes, result)}") # if min_dims >= 1 then core dims are never excluded # otherwise, should shrink towards excluding all optional dims expected_input_0 = ( (n_value,) if min_dims == 0 else (min_side,) * min_dims + (min_side, n_value) ) assert expected_input_0 == smallest_shapes[0] expected_input_1 = ( (n_value,) if min_dims == 0 else (min_side,) * min_dims + (n_value, min_side) ) assert expected_input_1 == smallest_shapes[1] def gufunc_sig_to_einsum_sig(gufunc_sig): """E.g. (i,j),(j,k)->(i,k) becomes ...ij,...jk->...ik""" def einlabels(labels): assert "x" not in labels, "we reserve x for fixed-dimensions" return "..." + "".join(i if not i.isdigit() else "x" for i in labels) gufunc_sig = _hypothesis_parse_gufunc_signature(gufunc_sig) input_sig = ",".join(map(einlabels, gufunc_sig.input_shapes)) return input_sig + "->" + einlabels(gufunc_sig.result_shape) @pytest.mark.parametrize( "gufunc_sig", [ param("()->()", id="unary sum"), param("(),()->()", id="binary sum"), param("(),(),()->()", id="trinary sum"), param("(i)->()", id="sum1d"), param("(i,j)->(j)", id="sum rows"), param("(i),(i)->()", id="inner1d"), param("(i),(i),(i)->()", id="trinary inner1d"), param("(m,n),(n,p)->(m,p)", id="matmat"), param("(n),(n,p)->(p)", id="vecmat"), param("(i,t),(j,t)->(i,j)", id="outer-inner"), param("(3),(3)->(3)", id="cross1d"), param("(i,j)->(j,i)", id="transpose"), param("(i),(j)->(i,j)", id="outer"), param("(i,3),(3,k)->(3,i,k)", id="fixed dim outer product"), param("(i),(j),(k)->(i,j,k)", id="trinary outer"), param("(i,i)->(i)", id="trace"), param("(j,i,i,j)->(i,j)", id="bigger trace"), param("(k),(j,i,k,i,j),(j,i)->(i,j)", id="trace product"), ], ) @given(data=st.data()) def test_einsum_gufunc_shapes(gufunc_sig, data): arrays, result_shape = data.draw( gufunc_arrays( nps.mutually_broadcastable_shapes(signature=gufunc_sig), dtype="float64", elements=st.floats(0, 1000), ), label="arrays, result_shape", ) out = np.einsum(gufunc_sig_to_einsum_sig(gufunc_sig), *arrays) assert out.shape == result_shape ================================================ FILE: hypothesis-python/tests/numpy/test_import.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. SHOULD_NOT_IMPORT_NUMPY = """ import sys from hypothesis import given, strategies as st @given(st.integers() | st.floats() | st.sampled_from(["a", "b"])) def test_no_numpy_import(x): assert "numpy" not in sys.modules """ def test_hypothesis_is_not_the_first_to_import_numpy(testdir): # We only import numpy if the user did so first. result = testdir.runpytest(testdir.makepyfile(SHOULD_NOT_IMPORT_NUMPY)) result.assert_outcomes(passed=1, failed=0) # We check the wildcard import works on the module level because that's the only # place Python actually allows us to use them. try: from hypothesis.extra.numpy import * # noqa: F403 star_import_works = True except AttributeError: star_import_works = False def test_wildcard_import(): assert star_import_works ================================================ FILE: hypothesis-python/tests/numpy/test_narrow_floats.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import numpy as np import pytest from hypothesis import given from hypothesis.extra.numpy import arrays, from_dtype, integer_dtypes from hypothesis.strategies import data, floats, integers @pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64]) @pytest.mark.parametrize("low", [-2.0, -1.0, 0.0, 1.0]) @given(data()) def test_bad_float_exclude_min_in_array(dtype, low, data): elements = floats( low, low + 1, exclude_min=True, width=np.dtype(dtype).itemsize * 8 ) x = data.draw(arrays(dtype, shape=(1,), elements=elements), label="x") assert np.all(low < x) @given(floats(width=32)) def test_float32_exactly_representable(x): clipped = np.dtype("float32").type(x) if np.isnan(x): assert np.isnan(clipped) else: assert x == float(clipped) @given(floats(width=16)) def test_float16_exactly_representable(x): clipped = np.dtype("float16").type(x) if np.isnan(x): assert np.isnan(clipped) else: assert x == float(clipped) @given(data=data(), dtype=integer_dtypes()) def test_floor_ceil_lossless(data, dtype): # Regression test for issue #1667; ceil converting numpy integers # to float and back to int with loss of exact value. x = data.draw(from_dtype(dtype)) assert data.draw(integers(x, x)) == x ================================================ FILE: hypothesis-python/tests/numpy/test_randomness.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import numpy as np from hypothesis import given from hypothesis.strategies import none def test_numpy_prng_is_seeded(): first = [] prng_state = np.random.get_state() @given(none()) def inner(_): val = np.random.bytes(10) if not first: first.append(val) assert val == first[0], "Numpy random module should be reproducible" inner() np.testing.assert_array_equal( np.random.get_state()[1], prng_state[1], "State was not restored." ) ================================================ FILE: hypothesis-python/tests/numpy/test_sampled_from.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given from hypothesis.errors import InvalidArgument from hypothesis.extra import numpy as npst from hypothesis.strategies import data, sampled_from from tests.common.utils import fails_with @given( data(), npst.arrays(dtype=npst.scalar_dtypes(), shape=npst.array_shapes(max_dims=1)) ) def test_can_sample_1D_numpy_array_without_warning(data, arr): data.draw(sampled_from(arr)) @fails_with(InvalidArgument) @given( data(), npst.arrays( dtype=npst.scalar_dtypes(), shape=npst.array_shapes(min_dims=2, max_dims=5) ), ) def test_sampling_multi_dimensional_arrays_is_deprecated(data, arr): data.draw(sampled_from(arr)) ================================================ FILE: hypothesis-python/tests/pandas/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/pandas/helpers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import numpy as np PANDAS_TIME_DTYPES = tuple(map(np.dtype, ["M8[ns]", ">m8[ns]"])) def supported_by_pandas(dtype): """Checks whether the dtype is one that can be correctly handled by Pandas.""" # Pandas does not support non-native byte orders and things go amusingly # wrong in weird places if you try to use them. See # https://pandas.pydata.org/pandas-docs/stable/gotchas.html#byte-ordering-issues if dtype.byteorder not in ("|", "="): return False # Pandas only supports a limited range of timedelta and datetime dtypes # compared to the full range that numpy supports and will convert # everything to those types (possibly increasing precision in the course of # doing so, which can cause problems if this results in something which # does not fit into the desired word type. As a result we want to filter # out any timedelta or datetime dtypes that are not of the desired types. if dtype.kind in ("m", "M"): return dtype in PANDAS_TIME_DTYPES return True ================================================ FILE: hypothesis-python/tests/pandas/test_argument_validation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re from datetime import datetime, timedelta import pandas as pd import pytest from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra import pandas as pdst from hypothesis.extra.pandas.impl import IntegerDtype from tests.common.arguments import argument_validation_test, e from tests.common.debug import check_can_generate_examples, find_any from tests.common.utils import checks_deprecated_behaviour BAD_ARGS = [ e(pdst.data_frames), e(pdst.data_frames, pdst.columns(1, dtype="not a dtype")), e(pdst.data_frames, pdst.columns(1, elements="not a strategy")), e(pdst.data_frames, pdst.columns([[]])), e(pdst.data_frames, [], index=[]), e(pdst.data_frames, [], rows=st.fixed_dictionaries({"A": st.just(1)})), e(pdst.data_frames, pdst.columns(1)), e(pdst.data_frames, pdst.columns(1, dtype=float, fill=1)), e(pdst.data_frames, pdst.columns(1, dtype=float, elements=1)), e(pdst.data_frames, pdst.columns(1, fill=1, dtype=float)), e(pdst.data_frames, pdst.columns(["A", "A"], dtype=float)), pytest.param( *e(pdst.data_frames, pdst.columns(1, elements=st.none(), dtype=int)), marks=pytest.mark.skipif(IntegerDtype, reason="works with integer NA"), ), e(pdst.data_frames, pdst.columns(1, elements=st.text(), dtype=int)), e(pdst.data_frames, 1), e(pdst.data_frames, [1]), e(pdst.data_frames, pdst.columns(1, dtype="category")), e( pdst.data_frames, pdst.columns(["A"], dtype=bool), rows=st.tuples(st.booleans(), st.booleans()), ), e( pdst.data_frames, pdst.columns(1, elements=st.booleans()), rows=st.tuples(st.booleans()), ), e(pdst.data_frames, rows=st.integers(), index=pdst.range_indexes(0, 0)), e(pdst.data_frames, rows=st.integers(), index=pdst.range_indexes(1, 1)), e(pdst.data_frames, pdst.columns(1, dtype=int), rows=st.integers()), e( pdst.data_frames, columns=pdst.columns(["a", "b"], dtype=str, elements=st.text()), rows=st.just({"a": "x"}), index=pdst.indexes(dtype=int, min_size=1), ), e( pdst.data_frames, columns=pdst.columns(["a", "b"], dtype=str, elements=st.text()), rows=st.just(["x"]), index=pdst.indexes(dtype=int, min_size=1), ), e(pdst.indexes), e(pdst.indexes, dtype="category"), e(pdst.indexes, dtype="not a dtype"), e(pdst.indexes, elements="not a strategy"), e(pdst.indexes, elements=st.text(), dtype=float), pytest.param( *e(pdst.indexes, elements=st.none(), dtype=int), marks=pytest.mark.skipif(IntegerDtype, reason="works with integer NA"), ), e(pdst.indexes, elements=st.text(), dtype=int), e(pdst.indexes, elements=st.just("9" * 30), dtype=int), # OverflowError handling e(pdst.indexes, elements=st.integers(0, 10), dtype=st.sampled_from([int, float])), e(pdst.indexes, dtype=int, max_size=0, min_size=1), e(pdst.indexes, dtype=int, unique="true"), e(pdst.indexes, dtype=int, min_size="0"), e(pdst.indexes, dtype=int, max_size="1"), e(pdst.range_indexes, 1, 0), e(pdst.range_indexes, min_size="0"), e(pdst.range_indexes, max_size="1"), e(pdst.range_indexes, name=""), e(pdst.series), e(pdst.series, dtype="not a dtype"), e(pdst.series, elements="not a strategy"), pytest.param( *e(pdst.series, elements=st.none(), dtype=int), marks=pytest.mark.skipif(IntegerDtype, reason="works with integer NA"), ), e(pdst.series, elements=st.text(), dtype=int), e(pdst.series, dtype="category"), e(pdst.series, index="not a strategy"), ] test_raise_invalid_argument = argument_validation_test(BAD_ARGS) lo, hi = pd.Timestamp(2017, 1, 1), pd.Timestamp(2084, 12, 21) @given(st.datetimes(min_value=lo, max_value=hi)) def test_timestamp_as_datetime_bounds(dt): # Would have caught https://github.com/HypothesisWorks/hypothesis/issues/2406 assert isinstance(dt, datetime) assert lo <= dt <= hi assert not isinstance(dt, pd.Timestamp) @checks_deprecated_behaviour def test_confusing_object_dtype_aliases(): check_can_generate_examples( pdst.series(elements=st.tuples(st.integers()), dtype=tuple) ) @pytest.mark.skipif( not IntegerDtype, reason="Nullable types not available in this version of Pandas" ) def test_pandas_nullable_types_class(): st = pdst.series(dtype=pd.core.arrays.integer.Int8Dtype) with pytest.raises( InvalidArgument, match="Otherwise it would be treated as dtype=object" ): find_any(st, lambda s: s.isna().any()) @pytest.mark.parametrize( "dtype_,expected_unit", [ (datetime, "datetime64[ns]"), (pd.Timestamp, "datetime64[ns]"), (timedelta, "timedelta64[ns]"), (pd.Timedelta, "timedelta64[ns]"), ], ) def test_invalid_datetime_or_timedelta_dtype_raises_error(dtype_, expected_unit): with pytest.raises(InvalidArgument, match=re.escape(expected_unit)): check_can_generate_examples(pdst.series(dtype=dtype_)) ================================================ FILE: hypothesis-python/tests/pandas/test_data_frame.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import numpy as np import pandas as pd import pytest from hypothesis import HealthCheck, given, reject, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra import numpy as npst, pandas as pdst from hypothesis.extra.pandas.impl import IntegerDtype from tests.common.debug import ( assert_all_examples, check_can_generate_examples, find_any, ) from tests.pandas.helpers import supported_by_pandas @given(pdst.data_frames([pdst.column("a", dtype=int), pdst.column("b", dtype=float)])) def test_can_have_columns_of_distinct_types(df): assert df["a"].dtype == np.dtype(int) assert df["b"].dtype == np.dtype(float) @given( pdst.data_frames( [pdst.column(dtype=int)], index=pdst.range_indexes(min_size=1, max_size=5) ) ) def test_respects_size_bounds(df): assert 1 <= len(df) <= 5 @given(pdst.data_frames(pdst.columns(["A", "B"], dtype=float))) def test_can_specify_just_column_names(df): df["A"] df["B"] @given(pdst.data_frames(pdst.columns(2, dtype=float))) def test_can_specify_just_column_count(df): df[0] df[1] @given( pdst.data_frames( rows=st.fixed_dictionaries({"A": st.integers(1, 10), "B": st.floats()}) ) ) def test_gets_the_correct_data_shape_for_just_rows(table): assert table["A"].dtype == np.dtype("int64") assert table["B"].dtype == np.dtype(float) @given( pdst.data_frames( columns=pdst.columns(["A", "B"], dtype=int), rows=st.lists(st.integers(0, 1000), min_size=2, max_size=2).map(sorted), ) ) def test_can_specify_both_rows_and_columns_list(d): assert d["A"].dtype == np.dtype(int) assert d["B"].dtype == np.dtype(int) for _, r in d.iterrows(): assert r["A"] <= r["B"] @given( pdst.data_frames( columns=pdst.columns(["A", "B"], dtype=int), rows=st.lists(st.integers(0, 1000), min_size=2, max_size=2) .map(sorted) .map(tuple), ) ) def test_can_specify_both_rows_and_columns_tuple(d): assert d["A"].dtype == np.dtype(int) assert d["B"].dtype == np.dtype(int) for _, r in d.iterrows(): assert r["A"] <= r["B"] @given( pdst.data_frames( columns=pdst.columns(["A", "B"], dtype=int), rows=st.lists(st.integers(0, 1000), min_size=2, max_size=2).map( lambda x: {"A": min(x), "B": max(x)} ), ) ) def test_can_specify_both_rows_and_columns_dict(d): assert d["A"].dtype == np.dtype(int) assert d["B"].dtype == np.dtype(int) for _, r in d.iterrows(): assert r["A"] <= r["B"] @given( pdst.data_frames( [ pdst.column( "A", fill=st.just(np.nan), dtype=float, elements=st.floats(allow_nan=False), ) ], rows=st.builds(dict), ) ) def test_can_fill_in_missing_elements_from_dict(df): assert np.isnan(df["A"]).all() @st.composite def column_strategy(draw): name = draw(st.none() | st.text()) dtype = draw(npst.scalar_dtypes().filter(supported_by_pandas)) pass_dtype = not draw(st.booleans()) if pass_dtype: pass_elements = not draw(st.booleans()) else: pass_elements = True if pass_elements: elements = npst.from_dtype(dtype) else: elements = None unique = draw(st.booleans()) fill = st.nothing() if draw(st.booleans()) else None return pdst.column( name=name, dtype=dtype, unique=unique, fill=fill, elements=elements ) @given(pdst.data_frames(pdst.columns(1, dtype=np.dtype("M8[ns]")))) def test_data_frames_with_timestamp_columns(df): pass @given( pdst.data_frames( pdst.columns(["A"], dtype=float, fill=st.just(np.nan), unique=True) ) ) def test_unique_column_with_fill(df): assert len(set(df["A"])) == len(df["A"]) @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much]) @given(st.data()) def test_arbitrary_data_frames(data): columns = data.draw( st.lists( column_strategy(), unique_by=lambda c: c.name if c.name is not None else np.nan, ) ) try: # Use raw data to work around pandas bug in repr. See # https://github.com/pandas-dev/pandas/issues/27484 df = data.conjecture_data.draw(pdst.data_frames(columns)) except Exception as e: if type(e).__name__ == "OutOfBoundsDatetime": # See https://github.com/HypothesisWorks/hypothesis-python/pull/826 reject() else: raise data_frame_columns = list(df) assert len(data_frame_columns) == len(columns) for i, (c, n) in enumerate(zip(columns, df, strict=True)): if c.name is None: assert n == i else: assert c.name == n for i, c in enumerate(columns): column_name = data_frame_columns[i] values = df[column_name] if c.unique: # NA values should always be treated as unique to each other, so we # just ignore them here. Note NA values in the ecosystem can have # different identity behaviours, e.g. # # >>> set([float("nan"), float("nan")]) # {nan, nan} # >>> set([pd.NaT, pd.NaT]) # {NaT} # non_na_values = values.dropna() assert len(set(non_na_values)) == len(non_na_values) @given( pdst.data_frames( pdst.columns(["A"], unique=True, dtype=int), rows=st.tuples(st.integers(0, 10)) ) ) def test_can_specify_unique_with_rows(df): column = df["A"] assert len(set(column)) == len(column) def test_uniqueness_does_not_affect_other_rows_1(): data_frames = pdst.data_frames( [ pdst.column("A", dtype=int, unique=True), pdst.column("B", dtype=int, unique=False), ], rows=st.tuples(st.integers(0, 10), st.integers(0, 10)), index=pdst.range_indexes(2, 2), ) find_any(data_frames, lambda x: x["B"][0] == x["B"][1]) def test_uniqueness_does_not_affect_other_rows_2(): data_frames = pdst.data_frames( [ pdst.column("A", dtype=bool, unique=False), pdst.column("B", dtype=int, unique=True), ], rows=st.tuples(st.booleans(), st.integers(0, 10)), index=pdst.range_indexes(2, 2), ) find_any(data_frames, lambda x: x["A"][0] == x["A"][1]) @given( pdst.data_frames(pdst.columns(["A"], dtype=int, fill=st.just(7)), rows=st.tuples()) ) def test_will_fill_missing_columns_in_tuple_row(df): for d in df["A"]: assert d == 7 @settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given( pdst.data_frames( index=pdst.range_indexes(10, 10), columns=[pdst.column(elements=st.integers(0, 9), fill=None, unique=True)], ) ) def test_can_generate_unique_columns(df): assert set(df[0]) == set(range(10)) @pytest.mark.skip(reason="Just works on Pandas 1.4, though the changelog is silent") @pytest.mark.parametrize("dtype", [None, object]) def test_expected_failure_from_omitted_object_dtype(dtype): # See https://github.com/HypothesisWorks/hypothesis/issues/3133 col = pdst.column(elements=st.sets(st.text(), min_size=1), dtype=dtype) @given(pdst.data_frames(columns=[col])) def works_with_object_dtype(df): pass if dtype is object: works_with_object_dtype() else: assert dtype is None with pytest.raises(ValueError, match="Maybe passing dtype=object would help"): works_with_object_dtype() @pytest.mark.skipif( not IntegerDtype, reason="Nullable types not available in this version of Pandas" ) def test_pandas_nullable_types(): st = pdst.data_frames(pdst.columns(2, dtype=pd.core.arrays.integer.Int8Dtype())) df = find_any(st, lambda s: s.isna().any().any()) for s in df.columns: assert type(df[s].dtype) == pd.core.arrays.integer.Int8Dtype @given(pdst.data_frames(columns=[pdst.column("col", dtype=object)])) def test_object_columns_are_of_type_object(df): assert df["col"].dtype == np.dtype("O") def test_class_instances_not_allowed_in_scalar_columns(): class A: pass s = pdst.data_frames( columns=[ pdst.column( "col", elements=st.just(A()), dtype=npst.scalar_dtypes(), ) ] ) with pytest.raises(InvalidArgument): check_can_generate_examples(s) def test_can_generate_object_arrays_with_mixed_dtype_elements(): class A: pass s = pdst.data_frames( columns=[pdst.column("col", st.just(A()) | st.integers(), dtype=object)], index=pdst.range_indexes(1), ) assert_all_examples(s, lambda df: df["col"].dtype == np.dtype("O")) find_any(s, lambda df: len({type(x) for x in df["col"].values}) > 1) @given(st.data()) @pytest.mark.xfail( strict=False, reason="not actually true due to pandas conversion. see " "https://github.com/HypothesisWorks/hypothesis/pull/4444#issuecomment-3413951478", ) def test_object_dataframe_can_hold_arbitrary_class_instances(data): instance = data.draw(st.from_type(type).flatmap(st.from_type)) s = pdst.data_frames( columns=[pdst.column("col", elements=st.just(instance), dtype=object)] ) df = data.draw(s) assert all(v is instance for v in df["col"].values) ================================================ FILE: hypothesis-python/tests/pandas/test_indexes.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys import numpy as np import pandas import pytest from hypothesis import HealthCheck, given, reject, settings, strategies as st from hypothesis.errors import Unsatisfiable from hypothesis.extra import numpy as npst, pandas as pdst from tests.common.debug import check_can_generate_examples from tests.pandas.helpers import supported_by_pandas # https://pandas.pydata.org/docs/whatsnew/v2.0.0.html#index-can-now-hold-numpy-numeric-dtypes @given(pdst.indexes(dtype=int, max_size=0)) def test_gets_right_dtype_for_empty_indices(ix): is_32bit = sys.maxsize == 2**31 - 1 pandas2 = pandas.__version__.startswith("2.") numpy1 = np.__version__.startswith("1.") windows = sys.platform == "win32" # including 64-bit windows, confusingly if pandas2 and (is_32bit or (windows and numpy1)): # No, I don't know what this is int32 on 64-bit windows until Numpy 2.0 assert ix.dtype == np.dtype("int32") else: assert ix.dtype == np.dtype("int64") @given(pdst.indexes(elements=st.integers(0, sys.maxsize), max_size=0)) def test_gets_right_dtype_for_empty_indices_with_elements(ix): assert ix.dtype == np.dtype("int64") def test_does_not_generate_impossible_conditions(): with pytest.raises(Unsatisfiable): check_can_generate_examples(pdst.indexes(min_size=3, max_size=3, dtype=bool)) @given(pdst.indexes(dtype=bool, unique=True)) def test_unique_indexes_of_small_values(ix): assert len(ix) <= 2 assert len(set(ix)) == len(ix) @given(pdst.indexes(dtype=bool, min_size=2, unique=True)) def test_unique_indexes_of_many_small_values(ix): assert len(ix) == 2 assert len(set(ix)) == len(ix) @given(pdst.indexes(dtype="int8", name=st.just("test_name"))) def test_name_passed_on_indexes(s): assert s.name == "test_name" # Sizes that fit into an int64 without overflow range_sizes = st.integers(0, 2**63 - 1) @given(range_sizes, range_sizes | st.none(), st.data()) def test_arbitrary_range_index(i, j, data): if j is not None: i, j = sorted((i, j)) data.draw(pdst.range_indexes(i, j)) @given(pdst.range_indexes(name=st.just("test_name"))) def test_name_passed_on_range_indexes(s): assert s.name == "test_name" @given(pdst.range_indexes()) def test_basic_range_indexes(ix): assert isinstance(ix, pandas.RangeIndex) @settings(suppress_health_check=[HealthCheck.too_slow]) @given(st.data()) def test_generate_arbitrary_indices(data): min_size = data.draw(st.integers(0, 10), "min_size") max_size = data.draw(st.none() | st.integers(min_size, min_size + 10), "max_size") unique = data.draw(st.booleans(), "unique") dtype = data.draw( st.one_of( npst.boolean_dtypes(), npst.integer_dtypes(endianness="="), npst.floating_dtypes(endianness="=", sizes=(32, 64)), npst.datetime64_dtypes(endianness="="), npst.timedelta64_dtypes(endianness="="), ).filter(supported_by_pandas), "dtype", ) pass_elements = data.draw(st.booleans(), "pass_elements") converted_dtype = pandas.Index([], dtype=dtype).dtype try: inferred_dtype = pandas.Index([data.draw(npst.from_dtype(dtype))]).dtype if pass_elements: elements = npst.from_dtype(dtype) dtype = None else: elements = None index = data.draw( pdst.indexes( elements=elements, dtype=dtype, min_size=min_size, max_size=max_size, unique=unique, ) ) except Exception as e: if type(e).__name__ == "OutOfBoundsDatetime": # See https://github.com/HypothesisWorks/hypothesis-python/pull/826 reject() else: raise if dtype is None: assert index.dtype == inferred_dtype else: assert index.dtype == converted_dtype if unique: assert len(set(index.values)) == len(index) ================================================ FILE: hypothesis-python/tests/pandas/test_series.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import numpy as np import pandas as pd import pytest from hypothesis import given, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.extra import numpy as nps, pandas as pdst from hypothesis.extra.pandas.impl import IntegerDtype from tests.common.debug import ( assert_all_examples, assert_no_examples, check_can_generate_examples, find_any, ) from tests.pandas.helpers import supported_by_pandas @given(st.data()) def test_can_create_a_series_of_any_dtype(data): dtype = data.draw(nps.scalar_dtypes().filter(supported_by_pandas)) # Use raw data to work around pandas bug in repr. See # https://github.com/pandas-dev/pandas/issues/27484 series = data.conjecture_data.draw(pdst.series(dtype=dtype)) assert series.dtype == dtype @given(pdst.series(dtype=object)) def test_can_create_a_series_of_object_python_type(series): assert series.dtype == np.dtype("O") @given( pdst.series( elements=nps.arrays( nps.array_dtypes() | nps.scalar_dtypes(), nps.array_shapes(), ), dtype=object, ) ) @settings(max_examples=5) def test_object_series_are_of_type_object(series): assert series.dtype == np.dtype("O") def test_class_instances_not_allowed_in_scalar_series(): class A: pass with pytest.raises(InvalidArgument): check_can_generate_examples( pdst.series(elements=st.just(A()), dtype=np.dtype("int")) ) def test_object_series_with_mixed_elements_still_has_object_dtype(): class A: pass s = nps.arrays( np.dtype("O"), shape=nps.array_shapes(), elements=st.just(A()) | st.integers(), ) assert_all_examples(s, lambda arr: arr.dtype == np.dtype("O")) find_any(s, lambda arr: len({type(x) for x in arr.ravel()}) > 1) @given(st.data()) @settings(max_examples=10) def test_series_can_hold_arbitrary_class_instances(data): instance = data.draw(st.from_type(type).flatmap(st.from_type)) s = pdst.series(elements=st.just(instance), dtype=object) series = data.draw(s) assert all(v is instance for v in series.values) @given(pdst.series(dtype=float, index=pdst.range_indexes(min_size=2, max_size=5))) def test_series_respects_size_bounds(s): assert 2 <= len(s) <= 5 def test_can_fill_series(): nan_backed = pdst.series(elements=st.floats(allow_nan=False), fill=st.just(np.nan)) find_any(nan_backed, lambda x: np.isnan(x).any()) @given(pdst.series(dtype=int)) def test_can_generate_integral_series(s): assert s.dtype == np.dtype(int) @given(pdst.series(elements=st.integers(0, 10))) def test_will_use_dtype_of_elements(s): assert s.dtype == np.dtype("int64") @given(pdst.series(elements=st.floats(allow_nan=False))) def test_will_use_a_provided_elements_strategy(s): assert not np.isnan(s).any() @given(pdst.series(dtype="int8", unique=True)) def test_unique_series_are_unique(s): assert len(s) == len(set(s)) @given(pdst.series(dtype="int8", name=st.just("test_name"))) def test_name_passed_on(s): assert s.name == "test_name" @pytest.mark.skipif( not IntegerDtype, reason="Nullable types not available in this version of Pandas" ) @pytest.mark.parametrize( "dtype", ["Int8", pd.core.arrays.integer.Int8Dtype() if IntegerDtype else None] ) def test_pandas_nullable_types(dtype): assert_no_examples( pdst.series(dtype=dtype, elements=st.just(0)), lambda s: s.isna().any(), ) assert_all_examples( pdst.series(dtype=dtype, elements=st.none()), lambda s: s.isna().all(), ) find_any(pdst.series(dtype=dtype), lambda s: not s.isna().any()) e = find_any(pdst.series(dtype=dtype), lambda s: s.isna().any()) assert type(e.dtype) == pd.core.arrays.integer.Int8Dtype ================================================ FILE: hypothesis-python/tests/patching/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/patching/callables.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """A stable file for which we can write patches. Don't move stuff around!""" from pathlib import Path import numpy as np from hypothesis import example, given, strategies as st from hypothesis.extra import numpy as npst WHERE = Path(__file__).relative_to(Path.cwd()) @given(st.integers()) def fn(x): """A trivial test function.""" class Cases: @example(n=0, label="whatever") @given(st.integers(), st.text()) def mth(self, n, label): """Indented method with existing example decorator.""" @given(st.integers()) @example(x=2).via("not a literal when repeated " * 2) @example(x=1).via("covering example") def covered(x): """A test function with a removable explicit example.""" @given(npst.arrays(np.int8, 1)) def undef_name(array): assert sum(array) < 100 # TODO: test function for insertion-order logic, once I get that set up. ================================================ FILE: hypothesis-python/tests/patching/test_patching.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re from datetime import datetime from pathlib import Path import pytest from hypothesis.extra._patching import ( FAIL_MSG, HEADER, get_patch_for, indent, make_patch, ) from hypothesis.internal.compat import WINDOWS from .callables import WHERE, Cases, covered, fn, undef_name from .toplevel import WHERE_TOP, fn_top from tests.common.utils import skipif_threading SIMPLE = ( fn, ("fn(\n x=1,\n)", FAIL_MSG), indent('@example(x=1).via("discovered failure")', prefix="+"), ) CASES = ( Cases.mth, ('mth(\n n=100,\n label="a long label which forces a newline",\n)', FAIL_MSG), indent( '@example(n=100, label="a long label which forces a newline")' '.via(\n "discovered failure"\n)', prefix="+ ", ), ) TOPLEVEL = ( fn_top, ("fn_top(\n x=1,\n)", FAIL_MSG), indent('@hypothesis.example(x=1).via("discovered failure")', prefix="+"), ) COVERING = ( covered, ("covered(\n x=0,\n)", "covering example"), indent('@example(x=1).via("covering example")', prefix="-") + "\n" + indent('@example(x=0).via("covering example")', prefix="+"), ) UNDEF_NAME = ( undef_name, ("undef_name(\n array=array([100], dtype=int8),\n)", FAIL_MSG), '+@example(array=np.array([100], dtype=np.int8)).via("discovered failure")', ) def strip_trailing_whitespace(s): """Patches have whitespace-only lines; strip that out.""" return re.sub(r" +$", "", s, flags=re.MULTILINE) @pytest.mark.parametrize( "tst, example, expected", [ pytest.param(*SIMPLE, id="simple"), pytest.param(*CASES, id="cases"), ], ) def test_adds_simple_patch(tst, example, expected): where, before, after = get_patch_for(tst, [example]) assert Path(where) == WHERE added = set(after.splitlines()) - set(before.splitlines()) assert added == {line.lstrip("+") for line in expected.splitlines()} SIMPLE_PATCH_BODY = f'''\ --- ./{WHERE} +++ ./{WHERE} @@ -21,6 +21,7 @@ @given(st.integers()) {{0}} def fn(x): """A trivial test function.""" ''' CASES_PATCH_BODY = f'''\ --- ./{WHERE} +++ ./{WHERE} @@ -28,6 +28,9 @@ class Cases: @example(n=0, label="whatever") @given(st.integers(), st.text()) {{0}} def mth(self, n, label): """Indented method with existing example decorator.""" ''' TOPLEVEL_PATCH_BODY = f'''\ --- ./{WHERE_TOP} +++ ./{WHERE_TOP} @@ -19,5 +19,6 @@ @hypothesis.given(st.integers()) {{0}} def fn_top(x): """A trivial test function.""" ''' COVERING_PATCH_BODY = f'''\ --- ./{WHERE} +++ ./{WHERE} @@ -34,7 +34,7 @@ @given(st.integers()) @example(x=2).via("not a literal when repeated " * 2) {{0}} def covered(x): """A test function with a removable explicit example.""" ''' UNDEF_NAME_PATCH_BODY = f"""\ --- ./{WHERE} +++ ./{WHERE} @@ -40,6 +40,7 @@ @given(npst.arrays(np.int8, 1)) {{0}} def undef_name(array): assert sum(array) < 100 """ @pytest.mark.parametrize( "tst, example, expected, body, remove", [ pytest.param(*SIMPLE, SIMPLE_PATCH_BODY, (), id="simple"), pytest.param(*CASES, CASES_PATCH_BODY, (), id="cases"), pytest.param(*TOPLEVEL, TOPLEVEL_PATCH_BODY, (), id="toplevel"), pytest.param( *COVERING, COVERING_PATCH_BODY, ("covering example",), id="covering" ), pytest.param(*UNDEF_NAME, UNDEF_NAME_PATCH_BODY, (), id="undef_name"), ], ) def test_make_full_patch(tst, example, expected, body, remove): when = datetime.now() msg = "a message from the test" author = "the patch author" expected = HEADER.format(when=when, msg=msg, author=author) + body.format(expected) triple = get_patch_for(tst, [example], strip_via=remove) got = make_patch([triple], when=when, msg=msg, author=author) stripped = strip_trailing_whitespace(got) assert stripped.splitlines() == expected.splitlines() @pytest.mark.parametrize("n", [0, 1, 2]) def test_invalid_syntax_cases_dropped(n): tst, (ex, via), _expected = SIMPLE example_ls = [(ex.replace("x=1", f"x={x}"), via) for x in range(n)] example_ls.insert(-1, ("fn(\n x=<__main__.Cls object at 0x>,\n)", FAIL_MSG)) got = get_patch_for(tst, example_ls) if n == 0: assert got is None, "no valid examples, and hence no patch" return where, _, after = got assert Path(where) == WHERE assert after.count("@example(x=") == n def test_no_example_for_data_strategy(): assert get_patch_for(fn, [("fn(data=data(...))", "msg")]) is None assert get_patch_for(fn, [("fn(123, data(...))", "msg")]) is None assert get_patch_for(fn, [("fn(data='data(...)')", "msg")]) is not None assert get_patch_for(fn, [("fn(Foo(data=data(...)))", "msg")]) is not None def test_patch_order_preserved(): (_fname, _before, after) = get_patch_for( fn, [("fn(a=1)", "msg"), ("fn(b=2)", "msg")] ) assert after.startswith( '@given(st.integers())\n@example(a=1).via("msg")\n@example(b=2).via("msg")' ) (_fname, _before, after) = get_patch_for( fn, [("fn(b=2)", "msg"), ("fn(a=1)", "msg")] ) assert after.startswith( '@given(st.integers())\n@example(b=2).via("msg")\n@example(a=1).via("msg")' ) def test_deduplicates_examples(): tst, example, expected = SIMPLE where, _, after = get_patch_for(tst, [example, example]) assert Path(where) == WHERE assert after.count(expected.lstrip("+")) == 1 def test_irretrievable_callable(): # Check that we return None instead of raising an exception old_module = fn.__module__ try: fn.__module__ = "this.does.not.exist" triple = get_patch_for(fn, [(SIMPLE[1], FAIL_MSG)]) finally: fn.__module__ = old_module assert triple is None TESTSCRIPT_DUMPS_PATCH = """ from hypothesis import Phase, given, settings, strategies as st @settings(phases=list(Phase)) @given(st.integers(0, 10), st.integers(0, 10)) def test_failing_pbt(x, y): assert not x """ ADDED_LINES = """ +@example( + x=1, + y=0, # or any other generated value +).via("discovered failure") """ @skipif_threading @pytest.mark.skipif(WINDOWS, reason="backslash support is tricky") def test_pytest_reports_patch_file_location(pytester): script = pytester.makepyfile(TESTSCRIPT_DUMPS_PATCH) result = pytester.runpytest(script) result.assert_outcomes(failed=1) fname_pat = r"\.hypothesis/patches/\d{4}-\d\d-\d\d--[0-9a-f]{8}.patch" pattern = f"`git apply ({fname_pat})` to add failing examples to your code\\." print(f"{pattern=}") print(f"result.stdout=\n{indent(str(result.stdout), ' ')}") fname = re.search(pattern, str(result.stdout)).group(1) patch = Path(pytester.path).joinpath(fname).read_text(encoding="utf-8") print(patch) assert ADDED_LINES in patch ================================================ FILE: hypothesis-python/tests/patching/toplevel.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """A stable file for which we can write patches. Don't move stuff around!""" from pathlib import Path import hypothesis import hypothesis.strategies as st WHERE_TOP = Path(__file__).relative_to(Path.cwd()) @hypothesis.given(st.integers()) def fn_top(x): """A trivial test function.""" ================================================ FILE: hypothesis-python/tests/pytest/test__pytest.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import subprocess import sys TESTSUITE = """ from hypothesis import given from hypothesis import strategies as st import _pytest @given(st.integers()) def test_a(n): pass test_a() """ def test_import_just_private_pytest_works(testdir): # importing just _pytest does not import its submodules like _pytest.outcomes. # Ensure we don't rely on this by writing conditional imports like # `if "_pytest" in sys.modules: sys.modules["_pytest"].outcomes`. script = testdir.makepyfile(TESTSUITE) result = subprocess.run( [sys.executable, script], # some hypothesis plugins (trio) import pytest, making this test pass # even when hypothesis has a bug. This was the case for my local installation, # and I am including this here on the off chance our CI setup has a similar # install situation. I don't want to wait for another regression to find out # this test doesn't work. env=os.environ | {"HYPOTHESIS_NO_PLUGINS": "1"}, ) assert result.returncode == 0 ================================================ FILE: hypothesis-python/tests/pytest/test_capture.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis._settings import _CI_VARS from hypothesis.internal.compat import WINDOWS, escape_unicode_characters pytest_plugins = "pytester" TESTSUITE = """ from hypothesis import given, settings, Verbosity from hypothesis.strategies import integers @settings(verbosity=Verbosity.verbose) @given(integers()) def test_should_be_verbose(x): pass """ @pytest.mark.parametrize("capture,expected", [("no", True), ("fd", False)]) def test_output_without_capture(testdir, capture, expected): script = testdir.makepyfile(TESTSUITE) result = testdir.runpytest(script, "--verbose", "--capture", capture) out = "\n".join(result.stdout.lines) assert "test_should_be_verbose" in out assert ("Trying example" in out) == expected assert result.ret == 0 UNICODE_EMITTING = """ import pytest from hypothesis import given, settings, Verbosity from hypothesis.strategies import text import sys def test_emits_unicode(): @settings(verbosity=Verbosity.verbose) @given(text()) def test_should_emit_unicode(t): assert all(ord(c) <= 1000 for c in t), ascii(t) with pytest.raises(AssertionError): test_should_emit_unicode() """ @pytest.mark.xfail( WINDOWS, reason="Encoding issues in running the subprocess, possibly pytest's fault", strict=False, # It's possible, if rare, for this to pass on Windows too. ) def test_output_emitting_unicode(testdir, monkeypatch): monkeypatch.setenv("LC_ALL", "C") monkeypatch.setenv("LANG", "C") script = testdir.makepyfile(UNICODE_EMITTING) result = getattr(testdir, "runpytest_subprocess", testdir.runpytest)( script, "--verbose", "--capture=no" ) out = "\n".join(result.stdout.lines) assert "test_emits_unicode" in out assert chr(1001) in out or escape_unicode_characters(chr(1001)) in out assert result.ret == 0 def get_line_num(token, lines, skip_n=0): skipped = 0 for i, line in enumerate(lines): if token in line: if skip_n == skipped: return i else: skipped += 1 raise AssertionError( f"Token {token!r} not found (skipped {skipped} of planned {skip_n} skips)" ) TRACEBACKHIDE_HEALTHCHECK = """ from hypothesis import given, settings from hypothesis.strategies import integers import time @given(integers().map(lambda x: time.sleep(0.2))) def test_healthcheck_traceback_is_hidden(x): pass """ def test_healthcheck_traceback_is_hidden(testdir, monkeypatch): for key in _CI_VARS: monkeypatch.delenv(key, raising=False) script = testdir.makepyfile(TRACEBACKHIDE_HEALTHCHECK) lines = testdir.runpytest(script, "--verbose").stdout.lines def_token = "__ test_healthcheck_traceback_is_hidden __" timeout_token = ": FailedHealthCheck" def_line = get_line_num(def_token, lines) timeout_line = get_line_num(timeout_token, lines) # 16 on pytest 8.4.0 combined with py3{9, 10} or 3.13 free-threading (but # not with 3.13 normal??) assert timeout_line - def_line in {15, 16} COMPOSITE_IS_NOT_A_TEST = """ from hypothesis.strategies import composite, none @composite def test_data_factory(draw): return draw(none()) """ def test_deprecation_of_strategies_as_tests(testdir): script = testdir.makepyfile(COMPOSITE_IS_NOT_A_TEST) testdir.runpytest(script).assert_outcomes(failed=1) ================================================ FILE: hypothesis-python/tests/pytest/test_checks.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. TEST_DECORATORS_ALONE = """ import hypothesis from hypothesis.strategies import composite, none @composite def test_composite_is_not_a_test(draw): # This strategy will be instantiated, but no draws == no calls. return draw(none()) @hypothesis.seed(0) def test_seed_without_given_fails(): pass @hypothesis.example(x=None) def test_example_without_given_fails(): pass @hypothesis.reproduce_failure(hypothesis.__version__, b"AA==") def test_repro_without_given_fails(): pass """ def test_decorators_without_given_should_fail(testdir): script = testdir.makepyfile(TEST_DECORATORS_ALONE) result = testdir.runpytest(script) result.assert_outcomes(failed=4) assert "pytest_runtest_call" not in "\n".join(result.outlines) ================================================ FILE: hypothesis-python/tests/pytest/test_collection_warning.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from tests.common.utils import skipif_threading pytest_plugins = "pytester" INI = """ [pytest] norecursedirs = .svn tmp whatever* """ TEST_SCRIPT = """ def test_noop(): pass """ @pytest.mark.skipif(int(pytest.__version__.split(".")[0]) < 7, reason="hook is new") @skipif_threading # colliding test_bad.py file def test_collection_warning(pytester): pytester.mkdir(".hypothesis") pytester.path.joinpath("pytest.ini").write_text(INI, encoding="utf-8") pytester.path.joinpath("test_ok.py").write_text(TEST_SCRIPT, encoding="utf-8") pytester.path.joinpath(".hypothesis/test_bad.py").write_text( TEST_SCRIPT.replace("pass", "raise Exception"), encoding="utf-8" ) result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1, warnings=1) assert "Skipping collection of '.hypothesis'" in "\n".join(result.outlines) ================================================ FILE: hypothesis-python/tests/pytest/test_compat.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import given from hypothesis.strategies import booleans @given(booleans()) @pytest.mark.parametrize("hi", (1, 2, 3)) def test_parametrize_after_given(hi, i): pass ================================================ FILE: hypothesis-python/tests/pytest/test_constant_collection_timing.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import pytest pytest_plugins = "pytester" CONFTEST = """ import time import hypothesis.internal.conjecture.providers as providers _called = False def slow_get_local_constants(): global _called if not _called: _called = True time.sleep(0.1) return providers._local_constants providers._get_local_constants = slow_get_local_constants providers._sys_modules_len = None """ TESTSUITE = """ from hypothesis import given, Phase, settings from hypothesis.strategies import integers from hypothesis.internal.conjecture.providers import _get_local_constants def test_first(): # Force constant collection to happen during this test _get_local_constants() @given(integers()) @settings(phases=[Phase.generate], max_examples=1, database=None) def test_second(x): pass """ @pytest.mark.parametrize("plugin_disabled", [False, True]) def test_constant_collection_timing(testdir, plugin_disabled): # See https://github.com/HypothesisWorks/hypothesis/issues/4627 testdir.makeconftest(CONFTEST) testdir.makepyfile(TESTSUITE) args = ["--durations=0", "-vv"] if plugin_disabled: args += ["-p", "no:hypothesispytest"] result = testdir.runpytest(*args) result.assert_outcomes(passed=2) output = "\n".join(result.stdout.lines) match = re.search(r"([\d.]+)s call\s+\S+::test_first", output) assert match, f"Could not find test_first timing in:\n{output}" test_first_time = float(match.group(1)) if plugin_disabled: assert test_first_time >= 0.05, f"took {test_first_time}s, expected >= 0.05s" else: assert test_first_time < 0.05, f"took {test_first_time}s, expected < 0.05s" ================================================ FILE: hypothesis-python/tests/pytest/test_doctest.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. pytest_plugins = "pytester" func_with_doctest = """ def hi(): ''' >>> i = 5 >>> i-1 4 ''' """ def test_can_run_doctests(testdir): script = testdir.makepyfile(func_with_doctest) result = testdir.runpytest(script, "--doctest-modules") assert result.ret == 0 ================================================ FILE: hypothesis-python/tests/pytest/test_fixtures.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from unittest.mock import Mock, create_autospec import pytest from hypothesis import example, given from hypothesis.strategies import integers from tests.common.utils import fails, skipif_threading pytest_plugins = "pytester" @pytest.fixture(scope="session") def infinity(): return float("inf") @pytest.fixture(scope="module") def mock_fixture(): return Mock() @pytest.fixture(scope="module") def spec_fixture(): class Foo: def __init__(self): pass def bar(self): return "baz" return create_autospec(Foo) @given(integers()) def test_can_mix_fixture_and_positional_strategy(infinity, xs): # Hypothesis fills arguments from the right, so if @given() uses # positional arguments then any strategies need to be on the right. assert xs <= infinity @given(xs=integers()) def test_can_mix_fixture_and_keyword_strategy(xs, infinity): assert xs <= infinity @example(xs=0) @given(xs=integers()) def test_can_mix_fixture_example_and_keyword_strategy(xs, infinity): assert xs <= infinity @fails @given(integers()) def test_can_inject_mock_via_fixture(mock_fixture, xs): """A negative test is better for this one - this condition uncovers a bug whereby the mock fixture is executed instead of the test body and always succeeds. If this test fails, then we know we've run the test body instead of the mock. """ raise AssertionError @given(integers()) def test_can_inject_autospecced_mock_via_fixture(spec_fixture, xs): spec_fixture.bar.return_value = float("inf") assert xs <= spec_fixture.bar() TESTSUITE = """ import pytest from hypothesis import given, strategies as st @pytest.fixture(scope="function", autouse=True) def autofix(request): pass @given(x=st.integers()) def test_requests_function_scoped_fixture(capsys, x): pass @pytest.mark.parametrize("percent", ["%", "%s"]) @given(x=st.integers()) def test_requests_function_scoped_fixture_percent_parametrized(capsys, x, percent): # See https://github.com/HypothesisWorks/hypothesis/issues/2469 pass class TestClass: @given(x=st.integers()) def test_requests_method_scoped_fixture(capsys, x): pass @given(x=st.integers()) def test_autouse_function_scoped_fixture(x): pass """ def test_given_plus_function_scoped_non_autouse_fixtures_are_deprecated(testdir): script = testdir.makepyfile(TESTSUITE) testdir.runpytest(script).assert_outcomes(passed=1, failed=4) CONFTEST_SUPPRESS = """ from hypothesis import HealthCheck, settings settings.register_profile( "suppress", suppress_health_check=[HealthCheck.function_scoped_fixture], ) """ @skipif_threading def test_suppress_fixture_health_check_via_profile(testdir): script = testdir.makepyfile(TESTSUITE) testdir.makeconftest(CONFTEST_SUPPRESS) testdir.runpytest(script).assert_outcomes(passed=1, failed=4) testdir.runpytest(script, "--hypothesis-profile=suppress").assert_outcomes(passed=5) TESTSCRIPT_SUPPRESS_FIXTURE = """ import pytest from hypothesis import HealthCheck, given, settings, strategies as st @given(x=st.integers()) def test_fails_health_check(capsys, x): pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) @given(x=st.integers()) def test_suppresses_health_check(capsys, x): pass @given(x=st.integers()) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) def test_suppresses_health_check_2(capsys, x): pass """ def test_suppress_health_check_function_scoped_fixture(testdir): script = testdir.makepyfile(TESTSCRIPT_SUPPRESS_FIXTURE) testdir.runpytest(script).assert_outcomes(passed=2, failed=1) TESTSCRIPT_OVERRIDE_FIXTURE = """ import pytest from hypothesis import given, strategies as st @pytest.fixture(scope="function", name="event_loop") def event_loop_1(): return @pytest.fixture(scope="module", name="event_loop") def event_loop_2(): return @given(x=st.integers()) def test_override_fixture(event_loop, x): pass """ def test_given_plus_overridden_fixture(testdir): script = testdir.makepyfile(TESTSCRIPT_OVERRIDE_FIXTURE) testdir.runpytest(script, "-Werror").assert_outcomes(passed=1, failed=0) TESTSCRIPT_FIXTURE_THEN_GIVEN = """ import pytest from hypothesis import given, strategies as st @given(x=st.integers()) @pytest.fixture() def test(x): pass """ pytest_version = tuple(map(int, pytest.__version__.split(".")[:2])) def assert_outcomes(result, *, errors=0, failed=0): kwargs = {"errors" if pytest_version[0] > 5 else "error": errors} result.assert_outcomes(failed=failed, **kwargs) def test_given_fails_if_already_decorated_with_fixture(testdir): script = testdir.makepyfile(TESTSCRIPT_FIXTURE_THEN_GIVEN) result = testdir.runpytest(script) if pytest_version[:2] >= (8, 4): assert_outcomes(result, errors=1) else: assert_outcomes(result, failed=1) TESTSCRIPT_GIVEN_THEN_FIXTURE = """ import pytest from hypothesis import given, strategies as st @pytest.fixture() @given(x=st.integers()) def test(x): pass """ def test_fixture_errors_if_already_decorated_with_given(testdir): script = testdir.makepyfile(TESTSCRIPT_GIVEN_THEN_FIXTURE) assert_outcomes(testdir.runpytest(script), errors=1) ================================================ FILE: hypothesis-python/tests/pytest/test_junit.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import xml.etree.ElementTree as ET from pathlib import Path pytest_plugins = "pytester" TESTSUITE = """ from hypothesis import given from hypothesis.strategies import integers @given(integers()) def test_valid(x): assert x == x @given(integers()) def test_invalid(x): assert x != x """ def _run_and_get_junit(testdir, *args): script = testdir.makepyfile(TESTSUITE) testdir.runpytest(script, "--junit-xml=out.xml", *args) return ET.parse(Path(testdir.tmpdir) / "out.xml").getroot() def _findall_from_root(junit_xml, path): if junit_xml.tag == "testsuites": return junit_xml.findall(f"./testsuite/{path}") # This case only exists for tests against Pytest before 5.1.0; # see https://github.com/pytest-dev/pytest/commit/a43ba78d3bde return junit_xml.findall(f"./{path}") def suite_properties_ok(junit_xml): # Check whether is included in . This is currently not # the case when using pytest-xdist, which is a shame, but we can live with it. testsuite_props = _findall_from_root(junit_xml, "properties") return len(testsuite_props) == 1 and { prop.get("name") for prop in testsuite_props[0].findall("property") } == { "hypothesis-statistics-test_outputs_valid_xunit2.py::test_valid", "hypothesis-statistics-test_outputs_valid_xunit2.py::test_invalid", } def test_outputs_valid_xunit2(testdir): # The thing we really care about with pytest-xdist + junitxml is that we don't # break xunit2 compatibility by putting inside . junit_xml = _run_and_get_junit(testdir) testcase_props = _findall_from_root(junit_xml, "testcase/properties") assert len(testcase_props) == 0 # Check whether is included in assert suite_properties_ok(junit_xml) def test_outputs_valid_xunit2_with_xdist(testdir): junit_xml = _run_and_get_junit(testdir, "-n2") testcase_props = _findall_from_root(junit_xml, "testcase/properties") assert len(testcase_props) == 0 # If is included in , this assertion will fail. # That would be a GOOD THING, and we would remove the `not` to prevent regressions. assert not suite_properties_ok(junit_xml) ================================================ FILE: hypothesis-python/tests/pytest/test_mark.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. pytest_plugins = "pytester" TESTSUITE = """ from hypothesis import given from hypothesis.strategies import integers @given(integers()) def test_foo(x): pass def test_bar(): pass """ def test_can_select_mark(testdir): script = testdir.makepyfile(TESTSUITE) result = testdir.runpytest( script, "--verbose", "--strict-markers", "-m", "hypothesis" ) out = "\n".join(result.stdout.lines) assert "1 passed, 1 deselected" in out UNITTEST_TESTSUITE = """ from hypothesis import given from hypothesis.strategies import integers from unittest import TestCase class TestStuff(TestCase): @given(integers()) def test_foo(self, x): pass def test_bar(self): pass """ def test_can_select_mark_on_unittest(testdir): script = testdir.makepyfile(UNITTEST_TESTSUITE) result = testdir.runpytest( script, "--verbose", "--strict-markers", "-m", "hypothesis" ) out = "\n".join(result.stdout.lines) assert "1 passed, 1 deselected" in out ================================================ FILE: hypothesis-python/tests/pytest/test_parametrized_db_keys.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import HealthCheck, given, settings, strategies as st DB_KEY_TESTCASE = """ from hypothesis import settings, given from hypothesis.database import InMemoryExampleDatabase from hypothesis.strategies import booleans import pytest DB = InMemoryExampleDatabase() @settings(database=DB) @given(booleans()) @pytest.mark.parametrize("hi", (1, 2, 3)) @pytest.mark.xfail() def test_dummy_for_parametrized_db_keys(hi, i): assert Fail # Test *must* fail for it to end up the database anyway def test_DB_keys_for_parametrized_test(): assert len(DB.data) == 3 """ def test_db_keys_for_parametrized_tests_are_unique(testdir): script = testdir.makepyfile(DB_KEY_TESTCASE) testdir.runpytest(script).assert_outcomes(xfailed=3, passed=1) @pytest.fixture(params=["a", "b"]) def fixt(request): return request.param class TestNoDifferingExecutorsHealthCheck: # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/3733 @given(x=st.text()) @pytest.mark.parametrize("i", range(2)) def test_method(self, x, i): pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) @given(x=st.text()) def test_method_fixture(self, x, fixt): pass ================================================ FILE: hypothesis-python/tests/pytest/test_profiles.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import importlib import pytest from _hypothesis_pytestplugin import LOAD_PROFILE_OPTION pytest_plugins = "pytester" CONFTEST = """ from hypothesis._settings import settings settings.register_profile("test", settings(max_examples=1)) """ TESTSUITE = """ from hypothesis import given from hypothesis.strategies import integers from hypothesis._settings import settings def test_this_one_is_ok(): assert settings().max_examples == 1 """ def test_does_not_run_reporting_hook_by_default(testdir): script = testdir.makepyfile(TESTSUITE) testdir.makeconftest(CONFTEST) result = testdir.runpytest(script, LOAD_PROFILE_OPTION, "test") out = "\n".join(result.stdout.lines) assert "1 passed" in out assert "hypothesis profile" not in out # importlib.metadata.version respects e.g. local -e installs of hypothesis, # and matches what `pytest` uses more closely than `hypothesis.__version__` # (which is the actually-latest version). assert importlib.metadata.version("hypothesis") in out @pytest.mark.parametrize("option", ["-v", "--hypothesis-verbosity=verbose"]) def test_runs_reporting_hook_in_any_verbose_mode(testdir, option): script = testdir.makepyfile(TESTSUITE) testdir.makeconftest(CONFTEST) result = testdir.runpytest(script, LOAD_PROFILE_OPTION, "test", option) out = "\n".join(result.stdout.lines) assert "1 passed" in out assert "max_examples=1" in out assert "hypothesis profile" in out assert importlib.metadata.version("hypothesis") in out ================================================ FILE: hypothesis-python/tests/pytest/test_pytest_detection.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import subprocess import sys from hypothesis import core def test_is_running_under_pytest(): assert core.running_under_pytest FILE_TO_RUN = """ import hypothesis.core as core assert not core.running_under_pytest """ def test_is_not_running_under_pytest(tmp_path): pyfile = tmp_path / "test.py" pyfile.write_text(FILE_TO_RUN, encoding="utf-8") subprocess.check_call([sys.executable, str(pyfile)]) DOES_NOT_IMPORT_HYPOTHESIS = """ import sys def test_pytest_plugin_does_not_import_hypothesis(): assert "hypothesis" not in sys.modules """ def test_plugin_does_not_import_pytest(testdir): testdir.makepyfile(DOES_NOT_IMPORT_HYPOTHESIS) testdir.runpytest_subprocess().assert_outcomes(passed=1) ================================================ FILE: hypothesis-python/tests/pytest/test_reporting.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest pytest_plugins = "pytester" TESTSUITE = """ from hypothesis import given from hypothesis.strategies import lists, integers @given(integers()) def test_this_one_is_ok(x): pass @given(lists(integers())) def test_hi(xs): assert False """ def test_runs_reporting_hook(testdir): script = testdir.makepyfile(TESTSUITE) result = testdir.runpytest(script, "--verbose") out = "\n".join(result.stdout.lines) assert "test_this_one_is_ok" in out assert "Captured stdout call" not in out assert "Falsifying example" in out assert result.ret != 0 TEST_EXCEPTIONGROUP = """ from hypothesis import given, strategies as st @given(x=st.booleans()) def test_fuzz_sorted(x): raise ValueError if x else TypeError """ @pytest.mark.parametrize("tb", ["auto", "long", "short", "native"]) def test_no_missing_reports(testdir, tb): script = testdir.makepyfile(TEST_EXCEPTIONGROUP) result = testdir.runpytest(script, f"--tb={tb}") out = "\n".join(result.stdout.lines) # If the False case is missing, that means we're not printing exception info. # See https://github.com/HypothesisWorks/hypothesis/issues/3430 With --tb=native, # we should show the full ExceptionGroup with *both* errors. assert "x=False" in out assert "x=True" in out or tb != "native" ================================================ FILE: hypothesis-python/tests/pytest/test_runs.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given from hypothesis.strategies import integers from tests.common.utils import fails @given(integers()) def test_ints_are_ints(x): pass @fails @given(integers()) def test_ints_are_floats(x): assert isinstance(x, float) ================================================ FILE: hypothesis-python/tests/pytest/test_seeding.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import pytest from hypothesis._settings import _CI_VARS from tests.common.utils import skipif_threading pytest_plugins = "pytester" TEST_SUITE = """ from hypothesis import given, settings, assume import hypothesis.strategies as st first = None @settings(database=None) @given(st.integers()) def test_fails_once(some_int): assume(abs(some_int) > 1000) global first if first is None: first = some_int assert some_int != first """ CONTAINS_SEED_INSTRUCTION = re.compile(r"--hypothesis-seed=\d+", re.MULTILINE) @skipif_threading @pytest.mark.parametrize("seed", [0, 42, "foo"]) def test_runs_repeatably_when_seed_is_set(seed, testdir): script = testdir.makepyfile(TEST_SUITE) results = [ testdir.runpytest( script, "--verbose", "--strict-markers", f"--hypothesis-seed={seed}", "-rN" ) for _ in range(2) ] failure_lines = [] for r in results: assert all("--hypothesis-seed" not in l for l in r.stdout.lines) failure_line = [l for l in r.stdout.lines if "some_int=" in l] # each iteration should fail assert len(failure_line) == 1 failure_lines.append(failure_line[0]) # all the same failure assert len(set(failure_lines)) == 1 HEALTH_CHECK_FAILURE = """ import os from hypothesis import given, strategies as st, assume, reject RECORD_EXAMPLES = if os.path.exists(RECORD_EXAMPLES): target = None with open(RECORD_EXAMPLES, "r", encoding="utf-8") as i: seen = set(map(int, i.read().strip().split("\\n"))) else: target = open(RECORD_EXAMPLES, "w", encoding="utf-8") @given(st.integers()) def test_failure(i): if target is None: assume(i not in seen) else: target.write(f"{i}\\n") reject() """ def test_repeats_healthcheck_when_following_seed_instruction( testdir, tmp_path, monkeypatch ): for key in _CI_VARS: monkeypatch.delenv(key, raising=False) health_check_test = HEALTH_CHECK_FAILURE.replace( "", repr(str(tmp_path / "seen")) ) script = testdir.makepyfile(health_check_test) initial = testdir.runpytest(script, "--verbose", "--strict-markers") match = CONTAINS_SEED_INSTRUCTION.search("\n".join(initial.stdout.lines)) initial_output = "\n".join(initial.stdout.lines) match = CONTAINS_SEED_INSTRUCTION.search(initial_output) assert match is not None rerun = testdir.runpytest(script, "--verbose", "--strict-markers", match.group(0)) rerun_output = "\n".join(rerun.stdout.lines) assert "FailedHealthCheck" in rerun_output assert "--hypothesis-seed" not in rerun_output rerun2 = testdir.runpytest( script, "--verbose", "--strict-markers", "--hypothesis-seed=10" ) rerun2_output = "\n".join(rerun2.stdout.lines) assert "FailedHealthCheck" not in rerun2_output ================================================ FILE: hypothesis-python/tests/pytest/test_sideeffect_warnings.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest pytest_plugins = "pytester" TEST_SCRIPT = """ def test_noop(): pass """ LAZY_STRATEGY = "integers()" SIDEEFFECT_STATEMENT = f"st.{LAZY_STRATEGY}.wrapped_strategy" SIDEEFFECT_SCRIPT = f""" from hypothesis import strategies as st {SIDEEFFECT_STATEMENT} """ @pytest.mark.skipif( tuple(map(int, pytest.__version__.split(".")[:2])) <= (6, 1), reason="Older pytest don't capture these warnings during runpytest setup", ) def test_sideeffect_warning(testdir): testdir.makeconftest(SIDEEFFECT_SCRIPT) script = testdir.makepyfile(TEST_SCRIPT) result = testdir.runpytest_subprocess(script) assert "HypothesisSideeffectWarning" in "\n".join(result.outlines) assert LAZY_STRATEGY in "\n".join(result.outlines) def test_conftest_sideeffect_pinpoint_error(testdir, monkeypatch): # -Werror is not sufficient since warning is emitted before session start. Additionally, we # don't want to raise errors from other plugins. Due to limited filtering capabilities of # PYTHONWARNINGS/-W ("message is a literal string that the start of the warning message must # contain" and only built-in categories), we must fall back to the actual message text. monkeypatch.setenv("PYTHONWARNINGS", "error:Slow code in plugin") testdir.makeconftest(SIDEEFFECT_SCRIPT) script = testdir.makepyfile(TEST_SCRIPT) result = testdir.runpytest_subprocess(script) assert "HypothesisSideeffectWarning" in "\n".join(result.errlines) # Plugin is always loaded before conftest, so "during pytest plugin initialization" assert "during pytest" in "\n".join(result.errlines) assert SIDEEFFECT_STATEMENT in "\n".join(result.errlines) def test_plugin_sideeffect_pinpoint_error(testdir, monkeypatch): # See comment above regarding this line monkeypatch.setenv("PYTHONWARNINGS", "error:Slow code in plugin") # Ensure we see the correct stacktrace regardless of plugin load order monkeypatch.setenv("HYPOTHESIS_EXTEND_INITIALIZATION", "1") testdir.makepyfile(sideeffect_plugin=SIDEEFFECT_SCRIPT) script = testdir.makepyfile(TEST_SCRIPT) result = testdir.runpytest_subprocess(script, "-p", "sideeffect_plugin") assert "HypothesisSideeffectWarning" in "\n".join(result.errlines) # Plugin order unknown, but certainly not at import time assert "at import time" not in "\n".join(result.errlines) assert SIDEEFFECT_STATEMENT in "\n".join(result.errlines) ================================================ FILE: hypothesis-python/tests/pytest/test_skipping.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. pytest_plugins = "pytester" PYTEST_TESTSUITE = """ from hypothesis import given from hypothesis.strategies import integers import pytest @given(xs=integers()) def test_to_be_skipped(xs): # We always try the simplest example first, raising a Skipped exception # which we know to propagate immediately... if xs == 0: pytest.skip() # But the pytest 3.0 internals don't have such an exception, so we keep # going and raise a BaseExceptionGroup error. Ah well. else: assert xs == 0 """ def test_no_falsifying_example_if_pytest_skip(testdir): """If ``pytest.skip() is called during a test, Hypothesis should not continue running the test and shrink process, nor should it print anything about falsifying examples.""" script = testdir.makepyfile(PYTEST_TESTSUITE) result = testdir.runpytest( script, "--verbose", "--strict-markers", "-m", "hypothesis" ) out = "\n".join(result.stdout.lines) assert "Falsifying example" not in out def test_issue_3453_regression(testdir): """If ``pytest.skip() is called during a test, Hypothesis should not continue running the test and shrink process, nor should it print anything about falsifying examples.""" script = testdir.makepyfile( """ from hypothesis import example, given, strategies as st import pytest @given(value=st.none()) @example("hello") @example("goodbye") def test_skip_on_first_skipping_example(value): assert value is not None assert value != "hello" # queue up a non-skip error which must be discarded pytest.skip() """ ) result = testdir.runpytest(script, "--tb=native") result.assert_outcomes(skipped=1) ================================================ FILE: hypothesis-python/tests/pytest/test_statistics.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from _hypothesis_pytestplugin import PRINT_STATISTICS_OPTION from tests.common.utils import skipif_threading pytest_plugins = "pytester" def get_output(testdir, suite, *args): script = testdir.makepyfile(suite) result = testdir.runpytest(script, *args) return "\n".join(result.stdout.lines) TESTSUITE = """ from hypothesis import HealthCheck, given, settings, assume from hypothesis.strategies import integers import time import warnings from hypothesis.errors import HypothesisDeprecationWarning warnings.simplefilter('always', HypothesisDeprecationWarning) @given(integers()) def test_all_valid(x): pass @settings(max_examples=100, suppress_health_check=list(HealthCheck)) @given(integers()) def test_iterations(x): assume(x == 13) """ def test_does_not_run_statistics_by_default(testdir): out = get_output(testdir, TESTSUITE) assert "Hypothesis Statistics" not in out def test_prints_statistics_given_option(testdir): out = get_output(testdir, TESTSUITE, PRINT_STATISTICS_OPTION) assert "Hypothesis Statistics" in out assert "max_examples=100" in out assert "< 1% of examples satisfied assumptions" in out def test_prints_statistics_given_option_under_xdist(testdir): out = get_output(testdir, TESTSUITE, PRINT_STATISTICS_OPTION, "-n", "2") assert "Hypothesis Statistics" in out assert "max_examples=100" in out assert "< 1% of examples satisfied assumptions" in out def test_prints_statistics_given_option_with_junitxml(testdir): out = get_output(testdir, TESTSUITE, PRINT_STATISTICS_OPTION, "--junit-xml=out.xml") assert "Hypothesis Statistics" in out assert "max_examples=100" in out assert "< 1% of examples satisfied assumptions" in out @skipif_threading @pytest.mark.skipif( tuple(map(int, pytest.__version__.split(".")[:2])) < (5, 4), reason="too old" ) def test_prints_statistics_given_option_under_xdist_with_junitxml(testdir): out = get_output( testdir, TESTSUITE, PRINT_STATISTICS_OPTION, "-n", "2", "--junit-xml=out.xml" ) assert "Hypothesis Statistics" in out assert "max_examples=100" in out assert "< 1% of examples satisfied assumptions" in out UNITTEST_TESTSUITE = """ from hypothesis import given from hypothesis.strategies import integers from unittest import TestCase class TestStuff(TestCase): @given(integers()) def test_all_valid(self, x): pass """ def test_prints_statistics_for_unittest_tests(testdir): script = testdir.makepyfile(UNITTEST_TESTSUITE) result = testdir.runpytest(script, PRINT_STATISTICS_OPTION) out = "\n".join(result.stdout.lines) assert "Hypothesis Statistics" in out assert "TestStuff::test_all_valid" in out assert "max_examples=100" in out STATEFUL_TESTSUITE = """ from hypothesis.stateful import RuleBasedStateMachine, rule class Stuff(RuleBasedStateMachine): @rule() def step(self): pass TestStuff = Stuff.TestCase """ def test_prints_statistics_for_stateful_tests(testdir): script = testdir.makepyfile(STATEFUL_TESTSUITE) result = testdir.runpytest(script, PRINT_STATISTICS_OPTION) out = "\n".join(result.stdout.lines) assert "Hypothesis Statistics" in out assert "TestStuff::runTest" in out assert "max_examples=100" in out ================================================ FILE: hypothesis-python/tests/quality/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/quality/test_deferred_strategies.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import strategies as st from tests.common.debug import minimal def test_large_branching_tree(): tree = st.deferred(lambda: st.integers() | st.tuples(tree, tree, tree, tree, tree)) assert minimal(tree) == 0 assert minimal(tree, lambda x: isinstance(x, tuple)) == (0,) * 5 def test_non_trivial_json(): json = st.deferred(lambda: st.none() | st.floats() | st.text() | lists | objects) lists = st.lists(json) objects = st.dictionaries(st.text(), json) assert minimal(json) is None assert minimal(json, lambda x: isinstance(x, list) and x) == [None] assert minimal( json, lambda x: isinstance(x, dict) and isinstance(x.get(""), list) ) == {"": []} def test_self_recursive_lists(): x = st.deferred(lambda: st.lists(x)) assert minimal(x) == [] assert minimal(x, bool) == [[]] assert minimal(x, lambda x: len(x) > 1) == [[], []] ================================================ FILE: hypothesis-python/tests/quality/test_discovery_ability.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. # -*- coding: utf-8 -*- """Statistical tests over the forms of the distributions in the standard set of definitions. These tests all take the form of a classic hypothesis test with the null hypothesis being that the probability of some event occurring when drawing data from the distribution produced by some specifier is >= REQUIRED_P """ import collections import math import re from hypothesis import HealthCheck, settings as Settings from hypothesis.control import BuildContext from hypothesis.errors import UnsatisfiedAssumption from hypothesis.internal import reflection from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.strategies import ( binary, booleans, floats, integers, just, lists, one_of, sampled_from, sets, text, tuples, ) from tests.common.utils import no_shrink from tests.conjecture.common import interesting_origin class HypothesisFalsified(AssertionError): pass def define_test(specifier, predicate, condition=None, p=0.5, suppress_health_check=()): runs = 100 required_runs = int(runs * p) def run_test(): if condition is None: def _condition(x): return True condition_string = "" else: _condition = condition condition_string = reflection.get_pretty_function_description(condition) condition_string = re.compile("^lambda[^:]*:\\s*").sub("", condition_string) def test_function(data): with BuildContext(data, wrapped_test=None): try: value = data.draw(specifier) except UnsatisfiedAssumption: data.mark_invalid() if not _condition(value): data.mark_invalid() if predicate(value): data.mark_interesting(interesting_origin()) successes = 0 actual_runs = 0 for actual_runs in range(1, runs + 1): # We choose the max_examples a bit larger than default so that we # run at least 100 examples outside of the small example generation # part of the generation phase. runner = ConjectureRunner( test_function, settings=Settings( max_examples=150, phases=no_shrink, suppress_health_check=suppress_health_check, ), ) runner.run() if runner.interesting_examples: successes += 1 if successes >= required_runs: return # If we reach a point where it's impossible to hit our target even # if every remaining attempt were to succeed, give up early and # report failure. if (required_runs - successes) > (runs - actual_runs): break event = reflection.get_pretty_function_description(predicate) if condition is not None: event += "|" event += condition_string raise HypothesisFalsified( f"P({event}) ~ {successes} / {actual_runs} = " f"{successes / actual_runs:.2f} < {required_runs / runs:.2f}; " "rejected" ) return run_test test_can_produce_zero = define_test(integers(), lambda x: x == 0) test_can_produce_large_magnitude_integers = define_test( integers(), lambda x: abs(x) > 1000 ) test_can_produce_large_positive_integers = define_test(integers(), lambda x: x > 1000) test_can_produce_large_negative_integers = define_test(integers(), lambda x: x < -1000) _factorials = {math.factorial(n) for n in range(9, 21)} def is_factorial(n): return abs(n) in _factorials test_can_produce_large_factorial = define_test( integers(), lambda n: n >= 50_000 and is_factorial(n), p=0.01 ) test_can_produce_above_large_factorial = define_test( integers(), lambda n: n >= 50_000 and is_factorial(n - 1), p=0.01 ) test_can_produce_below_large_factorial = define_test( integers(), lambda n: n >= 50_000 and is_factorial(n + 1), p=0.01 ) test_can_produce_large_factorial_negative = define_test( integers(), lambda n: n <= -50_000 and is_factorial(n), p=0.01 ) test_can_produce_above_large_factorial_negative = define_test( integers(), lambda n: n <= -50_000 and is_factorial(n - 1), p=0.01 ) test_can_produce_below_large_factorial_negative = define_test( integers(), lambda n: n <= -50_000 and is_factorial(n + 1), p=0.01 ) test_can_produce_unstripped_strings = define_test(text(), lambda x: x != x.strip()) test_can_produce_stripped_strings = define_test(text(), lambda x: x == x.strip()) # The pass probability here was previously 0.5, but some intermediate changes # while working on the ir tweaked the distribution and made it flaky. We can # reevaluate this once things have settled down, and likely bump the pass # probability back up. test_can_produce_multi_line_strings = define_test(text(), lambda x: "\n" in x, p=0.35) test_can_produce_ascii_strings = define_test( text(), lambda x: all(ord(c) <= 127 for c in x) ) test_can_produce_long_strings_with_no_ascii = define_test( text(min_size=5), lambda x: all(ord(c) > 127 for c in x), p=0.1 ) test_can_produce_short_strings_with_some_non_ascii = define_test( text(), lambda x: any(ord(c) > 127 for c in x), condition=lambda x: len(x) <= 3 ) test_can_produce_large_binary_strings = define_test( binary(), lambda x: len(x) > 10, p=0.3 ) test_can_produce_positive_infinity = define_test(floats(), lambda x: x == math.inf) test_can_produce_negative_infinity = define_test(floats(), lambda x: x == -math.inf) test_can_produce_nan = define_test(floats(), math.isnan) test_can_produce_floats_near_left = define_test(floats(0, 1), lambda t: t < 0.2) test_can_produce_floats_near_right = define_test(floats(0, 1), lambda t: t > 0.8) test_can_produce_floats_in_middle = define_test(floats(0, 1), lambda t: 0.2 <= t <= 0.8) test_can_produce_long_lists = define_test( lists(integers()), lambda x: len(x) >= 10, p=0.3 ) test_can_produce_short_lists = define_test(lists(integers()), lambda x: len(x) <= 10) test_can_produce_the_same_int_twice = define_test( lists(integers()), lambda t: len(set(t)) < len(t) ) def distorted_value(x): c = collections.Counter(x) return min(c.values()) * 3 <= max(c.values()) test_sampled_from_large_number_can_mix = define_test( lists(sampled_from(range(50)), min_size=50), lambda x: len(set(x)) >= 25 ) test_sampled_from_often_distorted = define_test( lists(sampled_from(range(5))), distorted_value, condition=lambda x: len(x) >= 3 ) test_non_empty_subset_of_two_is_usually_large = define_test( sets(sampled_from((1, 2))), lambda t: len(t) == 2 ) test_subset_of_ten_is_sometimes_empty = define_test( sets(integers(1, 10)), lambda t: len(t) == 0 ) test_mostly_sensible_floats = define_test(floats(), lambda t: t + 1 > t) test_mostly_largish_floats = define_test( floats(), lambda t: t + 1 > 1, condition=lambda x: x > 0 ) test_ints_can_occasionally_be_really_large = define_test( integers(), lambda t: t >= 2**63 ) test_mixing_is_sometimes_distorted = define_test( lists(booleans() | tuples()), lambda x: distorted_value(map(type, x)), condition=lambda x: len(set(map(type, x))) == 2, suppress_health_check=[HealthCheck.filter_too_much], ) test_mixes_2_reasonably_often = define_test( lists(booleans() | tuples()), lambda x: len(set(map(type, x))) > 1, condition=bool ) test_partial_mixes_3_reasonably_often = define_test( lists(booleans() | tuples() | just("hi")), lambda x: 1 < len(set(map(type, x))) < 3, condition=bool, ) test_mixes_not_too_often = define_test( lists(booleans() | tuples()), lambda x: len(set(map(type, x))) == 1, condition=bool ) test_integers_are_usually_non_zero = define_test(integers(), lambda x: x != 0) test_integers_are_sometimes_zero = define_test(integers(), lambda x: x == 0) test_integers_are_often_small = define_test(integers(), lambda x: abs(x) <= 100) test_integers_are_often_small_but_not_that_small = define_test( integers(), lambda x: 50 <= abs(x) <= 255 ) # This series of tests checks that the one_of() strategy flattens branches # correctly. We assert that the probability of any branch is >= 0.1, # approximately (1/8 = 0.125), regardless of how heavily nested it is in the # strategy. # This first strategy chooses an integer between 0 and 7 (inclusive). one_of_nested_strategy = one_of( just(0), one_of( just(1), just(2), one_of(just(3), just(4), one_of(just(5), just(6), just(7))) ), ) for i in range(8): exec(f"""test_one_of_flattens_branches_{i} = define_test( one_of_nested_strategy, lambda x: x == {i} )""") xor_nested_strategy = just(0) | ( just(1) | just(2) | (just(3) | just(4) | (just(5) | just(6) | just(7))) ) for i in range(8): exec(f"""test_xor_flattens_branches_{i} = define_test( xor_nested_strategy, lambda x: x == {i} )""") # This strategy tests interactions with `map()`. They generate integers # from the set {1, 4, 6, 16, 20, 24, 28, 32}. def double(x): return x * 2 one_of_nested_strategy_with_map = one_of( just(1), one_of( (just(2) | just(3)).map(double), one_of( (just(4) | just(5)).map(double), one_of((just(6) | just(7) | just(8)).map(double)), ).map(double), ), ) for i in (1, 4, 6, 16, 20, 24, 28, 32): exec(f"""test_one_of_flattens_map_branches_{i} = define_test( one_of_nested_strategy_with_map, lambda x: x == {i} )""") # This strategy tests interactions with `flatmap()`. It generates lists # of length 0-7 (inclusive) in which every element is `None`. one_of_nested_strategy_with_flatmap = just(None).flatmap( lambda x: one_of( just([x] * 0), just([x] * 1), one_of( just([x] * 2), just([x] * 3), one_of(just([x] * 4), just([x] * 5), one_of(just([x] * 6), just([x] * 7))), ), ) ) for i in range(8): exec(f"""test_one_of_flattens_flatmap_branches_{i} = define_test( one_of_nested_strategy_with_flatmap, lambda x: len(x) == {i} )""") xor_nested_strategy_with_flatmap = just(None).flatmap( lambda x: ( just([x] * 0) | just([x] * 1) | ( just([x] * 2) | just([x] * 3) | (just([x] * 4) | just([x] * 5) | (just([x] * 6) | just([x] * 7))) ) ) ) for i in range(8): exec(f"""test_xor_flattens_flatmap_branches_{i} = define_test( xor_nested_strategy_with_flatmap, lambda x: len(x) == {i} )""") # This strategy tests interactions with `filter()`. It generates the even # integers {0, 2, 4, 6} in equal measures. one_of_nested_strategy_with_filter = one_of( just(0), just(1), one_of(just(2), just(3), one_of(just(4), just(5), one_of(just(6), just(7)))), ).filter(lambda x: x % 2 == 0) for i in range(4): exec(f"""test_one_of_flattens_filter_branches_{i} = define_test( one_of_nested_strategy_with_filter, lambda x: x == 2 * {i} )""") test_long_duplicates_strings = define_test( tuples(text(), text()), lambda s: len(s[0]) >= 5 and s[0] == s[1] ) test_can_produce_nasty_strings = define_test( text(), lambda s: s in {"NaN", "Inf", "undefined"}, p=0.01 ) ================================================ FILE: hypothesis-python/tests/quality/test_float_shrinking.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesis import HealthCheck, example, given, settings, strategies as st from hypothesis.internal.compat import ceil from tests.common.debug import minimal def test_shrinks_to_simple_floats(): assert minimal(st.floats(), lambda x: x > 1) == 2.0 assert minimal(st.floats(), lambda x: x > 0) == 1.0 @pytest.mark.parametrize("n", [1, 2, 3, 8, 10]) def test_can_shrink_in_variable_sized_context(n): x = minimal(st.lists(st.floats(), min_size=n), any) assert len(x) == n assert x.count(0.0) == n - 1 assert 1 in x @example(1.7976931348623157e308) @example(1.5) @given(st.floats(min_value=0, allow_infinity=False, allow_nan=False)) @settings(suppress_health_check=[HealthCheck.nested_given]) def test_shrinks_downwards_to_integers(f): assert minimal(st.floats(min_value=f)) == ceil(f) @example(1) @given(st.integers(1, 2**16 - 1)) @settings(suppress_health_check=[HealthCheck.nested_given]) def test_shrinks_downwards_to_integers_when_fractional(b): g = minimal( st.floats( min_value=b, max_value=2**53, exclude_min=True, exclude_max=True ).filter(lambda x: int(x) != x) ) assert g == b + 0.5 ================================================ FILE: hypothesis-python/tests/quality/test_integers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import given, settings, strategies as st def test_biases_towards_boundary_values(): trillion = 10**12 boundary_vals = {-trillion, -trillion + 1, trillion - 1, trillion} @given(st.integers(-trillion, trillion)) @settings(max_examples=1000) def f(n): boundary_vals.discard(n) f() assert ( not boundary_vals ), f"Expected to see all boundary vals, but still have {boundary_vals}" ================================================ FILE: hypothesis-python/tests/quality/test_poisoned_lists.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from random import Random import pytest from hypothesis import settings, strategies as st from hypothesis.internal.compat import ceil from hypothesis.internal.conjecture.engine import ConjectureData, ConjectureRunner from hypothesis.strategies._internal import SearchStrategy from tests.conjecture.common import interesting_origin POISON = "POISON" class Poisoned(SearchStrategy): def __init__(self, poison_chance): super().__init__() self.__poison_chance = poison_chance self.__ints = st.integers(0, 10) def do_draw(self, data): if data.draw_boolean(self.__poison_chance): return POISON else: return data.draw(self.__ints) class LinearLists(SearchStrategy): def __init__(self, elements, size): super().__init__() self.__length = st.integers(0, size) self.__elements = elements def do_draw(self, data): return [data.draw(self.__elements) for _ in range(data.draw(self.__length))] class Matrices(SearchStrategy): def __init__(self, elements, size): super().__init__() self.__length = st.integers(0, ceil(size**0.5)) self.__elements = elements def do_draw(self, data): n = data.draw(self.__length) m = data.draw(self.__length) return [data.draw(self.__elements) for _ in range(n * m)] LOTS = 10**6 TRIAL_SETTINGS = settings(max_examples=LOTS, database=None) @pytest.mark.parametrize( "seed", [2282791295271755424, 1284235381287210546, 14202812238092722246, 26097] ) @pytest.mark.parametrize("size", [5, 10, 20]) @pytest.mark.parametrize("p", [0.01, 0.1]) @pytest.mark.parametrize("strategy_class", [LinearLists, Matrices]) def test_minimal_poisoned_containers(seed, size, p, strategy_class): elements = Poisoned(p) strategy = strategy_class(elements, size) def test_function(data): v = data.draw(strategy) data.output = repr(v) if POISON in v: data.mark_interesting(interesting_origin()) runner = ConjectureRunner( test_function, random=Random(seed), settings=TRIAL_SETTINGS ) runner.run() (v,) = runner.interesting_examples.values() result = ConjectureData.for_choices(v.choices).draw(strategy) assert len(result) == 1 ================================================ FILE: hypothesis-python/tests/quality/test_poisoned_trees.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from random import Random import pytest from hypothesis import HealthCheck, settings from hypothesis.internal.conjecture.engine import ConjectureData, ConjectureRunner from hypothesis.strategies._internal import SearchStrategy from tests.conjecture.common import interesting_origin POISON = "POISON" MAX_INT = 2**32 - 1 class PoisonedTree(SearchStrategy): """Generates variable sized tuples with an implicit tree structure. The actual result is flattened out, but the hierarchy is implicit in the data. """ def __init__(self, p): super().__init__() self.__p = p def do_draw(self, data): if data.draw_boolean(self.__p): return data.draw(self) + data.draw(self) else: # We draw n as two separate calls so that it doesn't show up as a # single choice. If it did, the heuristics that allow us to move # choices around would fire and it would move right, which would # then allow us to shrink it more easily. n1 = data.draw_integer(0, 2**16 - 1) << 16 n2 = data.draw_integer(0, 2**16 - 1) n = n1 | n2 if n == MAX_INT: return (POISON,) else: return (None,) TEST_SETTINGS = settings( database=None, suppress_health_check=list(HealthCheck), max_examples=10**6, deadline=None, ) @pytest.mark.parametrize("size", [2, 5, 10]) @pytest.mark.parametrize("seed", [0, 15993493061449915028]) def test_can_reduce_poison_from_any_subtree(size, seed): """This test validates that we can minimize to any leaf node of a binary tree, regardless of where in the tree the leaf is.""" random = Random(seed) # Initially we create the minimal tree of size n, regardless of whether it # is poisoned (which it won't be - the poison event essentially never # happens when drawing uniformly at random). # Choose p so that the expected size of the tree is equal to the desired # size. p = 1.0 / (2.0 - 1.0 / size) strat = PoisonedTree(p) def test_function(data): v = data.draw(strat) if len(v) >= size: data.mark_interesting(interesting_origin()) runner = ConjectureRunner(test_function, random=random, settings=TEST_SETTINGS) runner.generate_new_examples() runner.shrink_interesting_examples() (data,) = runner.interesting_examples.values() assert len(ConjectureData.for_choices(data.choices).draw(strat)) == size # find the nodes corresponding to n1 and n2 nodes = [ node for node in data.nodes if node.type == "integer" and node.constraints["max_value"] == 2**16 - 1 ] assert len(nodes) % 2 == 0 marker = bytes([1, 2, 3, 4]) for i in range(0, len(nodes), 2): # Now for each leaf position in the tree we try inserting a poison # value artificially. Additionally, we add a marker to the end that # must be preserved. The marker means that we are not allow to rely on # discarding the end of the choice sequence to get the desired shrink. node = nodes[i] def test_function_with_poison(data): v = data.draw(strat) m = data.draw_bytes(len(marker), len(marker)) if POISON in v and m == marker: data.mark_interesting(interesting_origin()) runner = ConjectureRunner( test_function_with_poison, random=random, settings=TEST_SETTINGS ) # replace n1 and n2 with 2**16 - 1 to insert a poison value here runner.cached_test_function( data.choices[: node.index] + (2**16 - 1, 2**16 - 1) + (data.choices[node.index + 2 :]) + (marker,) ) assert runner.interesting_examples runner.shrink_interesting_examples() (shrunk,) = runner.interesting_examples.values() assert ConjectureData.for_choices(shrunk.choices).draw(strat) == (POISON,) ================================================ FILE: hypothesis-python/tests/quality/test_shrink_quality.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from collections import Counter, OrderedDict, namedtuple from fractions import Fraction from functools import reduce import pytest import hypothesis.strategies as st from hypothesis import HealthCheck, assume, given, settings from hypothesis.strategies import ( booleans, builds, dictionaries, fixed_dictionaries, fractions, frozensets, integers, just, lists, none, sampled_from, sets, text, tuples, ) from tests.common.debug import minimal from tests.common.utils import flaky def test_integers_from_minimizes_leftwards(): assert minimal(integers(min_value=101)) == 101 def test_minimize_bounded_integers_to_zero(): assert minimal(integers(-10, 10)) == 0 def test_minimize_bounded_integers_to_positive(): zero = 0 def not_zero(x): return x != zero assert minimal(integers(-10, 10).filter(not_zero)) == 1 def test_minimal_fractions_1(): assert minimal(fractions()) == Fraction(0) def test_minimal_fractions_2(): assert minimal(fractions(), lambda x: x >= 1) == Fraction(1) def test_minimal_fractions_3(): assert minimal(lists(fractions()), lambda s: len(s) >= 5) == [Fraction(0)] * 5 def test_minimize_string_to_empty(): assert minimal(text()) == "" def test_minimize_one_of(): for _ in range(100): assert minimal(integers() | text() | booleans()) in (0, "", False) def test_minimize_mixed_list(): mixed = minimal(lists(integers() | text()), lambda x: len(x) >= 10) assert set(mixed).issubset({0, ""}) def test_minimize_longer_string(): assert minimal(text(), lambda x: len(x) >= 10) == "0" * 10 def test_minimize_longer_list_of_strings(): assert minimal(lists(text()), lambda x: len(x) >= 10) == [""] * 10 def test_minimize_3_set(): assert minimal(sets(integers()), lambda x: len(x) >= 3) in ({0, 1, 2}, {-1, 0, 1}) def test_minimize_3_set_of_tuples(): assert minimal(sets(tuples(integers())), lambda x: len(x) >= 2) == {(0,), (1,)} def test_minimize_sets_of_sets(): elements = integers(1, 100) size = 8 set_of_sets = minimal(sets(frozensets(elements), min_size=size)) assert frozenset() in set_of_sets assert len(set_of_sets) == size for s in set_of_sets: if len(s) > 1: assert any(s != t and t.issubset(s) for t in set_of_sets) def test_minimize_sets_sampled_from(): assert minimal(st.sets(st.sampled_from(range(10)), min_size=3)) == {0, 1, 2} def test_can_simplify_flatmap_with_bounded_left_hand_size(): assert ( minimal(booleans().flatmap(lambda x: lists(just(x))), lambda x: len(x) >= 10) == [False] * 10 ) def test_can_simplify_across_flatmap_of_just(): assert minimal(integers().flatmap(just)) == 0 def test_can_simplify_on_right_hand_strategy_of_flatmap(): assert minimal(integers().flatmap(lambda x: lists(just(x)))) == [] @flaky(min_passes=5, max_runs=5) def test_can_ignore_left_hand_side_of_flatmap(): assert ( minimal(integers().flatmap(lambda x: lists(integers())), lambda x: len(x) >= 10) == [0] * 10 ) def test_can_simplify_on_both_sides_of_flatmap(): assert ( minimal(integers().flatmap(lambda x: lists(just(x))), lambda x: len(x) >= 10) == [0] * 10 ) def test_flatmap_rectangles(): lengths = integers(min_value=0, max_value=10) def lists_of_length(n): return lists(sampled_from("ab"), min_size=n, max_size=n) xs = minimal( lengths.flatmap(lambda w: lists(lists_of_length(w))), lambda x: ["a", "b"] in x, settings=settings(database=None, max_examples=2000), ) assert xs == [["a", "b"]] @flaky(min_passes=5, max_runs=5) @pytest.mark.parametrize("dict_class", [dict, OrderedDict]) def test_dictionary(dict_class): assert ( minimal(dictionaries(keys=integers(), values=text(), dict_class=dict_class)) == dict_class() ) x = minimal( dictionaries(keys=integers(), values=text(), dict_class=dict_class), lambda t: len(t) >= 3, ) assert isinstance(x, dict_class) assert set(x.values()) == {""} for k in x: if k < 0: assert k + 1 in x if k > 0: assert k - 1 in x def test_minimize_single_element_in_silly_large_int_range(): assert minimal(integers(-(2**256), 2**256), lambda x: x >= -(2**255)) == 0 def test_minimize_multiple_elements_in_silly_large_int_range(): actual = minimal( lists(integers(-(2**256), 2**256)), lambda x: len(x) >= 20, settings(max_examples=10_000), ) assert actual == [0] * 20 def test_minimize_multiple_elements_in_silly_large_int_range_min_is_not_dupe(): target = list(range(20)) actual = minimal( lists(integers(0, 2**256)), lambda x: (assume(len(x) >= 20) and all(x[i] >= target[i] for i in target)), ) assert actual == target def test_find_large_union_list(): size = 10 def large_mostly_non_overlapping(xs): union = reduce(set.union, xs) return len(union) >= size result = minimal( lists(sets(integers(), min_size=1), min_size=1), large_mostly_non_overlapping, ) assert len(result) == 1 union = reduce(set.union, result) assert len(union) == size assert max(union) == min(union) + len(union) - 1 @pytest.mark.parametrize("n", [0, 1, 10, 100, 1000]) @pytest.mark.parametrize( "seed", [13878544811291720918, 15832355027548327468, 12901656430307478246] ) def test_containment(n, seed): iv = minimal( tuples(lists(integers()), integers()), lambda x: x[1] in x[0] and x[1] >= n, ) assert iv == ([n], n) def test_duplicate_containment(): ls, i = minimal( tuples(lists(integers()), integers()), lambda s: s[0].count(s[1]) > 1, ) assert ls == [0, 0] assert i == 0 @pytest.mark.parametrize("seed", [11, 28, 37]) def test_reordering_bytes(seed): ls = minimal(lists(integers()), lambda x: sum(x) >= 10 and len(x) >= 3) assert ls == sorted(ls) def test_minimize_long_list(): assert ( minimal(lists(booleans(), min_size=50), lambda x: len(x) >= 70) == [False] * 70 ) def test_minimize_list_of_longish_lists(): size = 5 xs = minimal( lists(lists(booleans())), lambda x: len([t for t in x if any(t) and len(t) >= 2]) >= size, ) assert len(xs) == size for x in xs: assert x == [False, True] def test_minimize_list_of_fairly_non_unique_ints(): xs = minimal(lists(integers()), lambda x: len(set(x)) < len(x)) assert len(xs) == 2 def test_list_with_complex_sorting_structure(): xs = minimal( lists(lists(booleans())), lambda x: [list(reversed(t)) for t in x] > x and len(x) > 3, ) assert len(xs) == 4 def test_list_with_wide_gap(): xs = minimal(lists(integers()), lambda x: x and (max(x) > min(x) + 10 > 0)) assert len(xs) == 2 xs.sort() assert xs[1] == 11 + xs[0] def test_minimize_namedtuple(): T = namedtuple("T", ("a", "b")) tab = minimal(builds(T, integers(), integers()), lambda x: x.a < x.b) assert tab.b == tab.a + 1 def test_minimize_dict(): tab = minimal( fixed_dictionaries({"a": booleans(), "b": booleans()}), lambda x: x["a"] or x["b"], ) assert not (tab["a"] and tab["b"]) def test_minimize_list_of_sets(): assert minimal( lists(sets(booleans())), lambda x: len(list(filter(None, x))) >= 3 ) == ([{False}] * 3) def test_minimize_list_of_lists(): assert minimal( lists(lists(integers())), lambda x: len(list(filter(None, x))) >= 3 ) == ([[0]] * 3) def test_minimize_list_of_tuples(): xs = minimal(lists(tuples(integers(), integers())), lambda x: len(x) >= 2) assert xs == [(0, 0), (0, 0)] def test_minimize_multi_key_dicts(): assert minimal(dictionaries(keys=booleans(), values=booleans()), bool) == { False: False } def test_multiple_empty_lists_are_independent(): x = minimal(lists(lists(none(), max_size=0)), lambda t: len(t) >= 2) u, v = x assert u is not v def test_can_find_sets_unique_by_incomplete_data(): size = 5 ls = minimal( lists(tuples(integers(), integers()), unique_by=max), lambda x: len(x) >= size ) assert len(ls) == size values = sorted(map(max, ls)) assert values[-1] - values[0] == size - 1 for u, _ in ls: assert u <= 0 @pytest.mark.parametrize("n", range(10)) def test_lists_forced_near_top(n): assert minimal( lists(integers(), min_size=n, max_size=n + 2), lambda t: len(t) == n + 2 ) == [0] * (n + 2) def test_sum_of_pair_int(): assert minimal( tuples(integers(0, 1000), integers(0, 1000)), lambda x: sum(x) > 1000 ) == (1, 1000) def test_sum_of_pair_float(): assert minimal( tuples(st.floats(0, 1000), st.floats(0, 1000)), lambda x: sum(x) > 1000 ) == (1.0, 1000.0) def test_sum_of_pair_mixed(): # check both orderings assert minimal( tuples(st.floats(0, 1000), st.integers(0, 1000)), lambda x: sum(x) > 1000 ) == (1.0, 1000) assert minimal( tuples(st.integers(0, 1000), st.floats(0, 1000)), lambda x: sum(x) > 1000 ) == (1, 1000.0) def test_sum_of_pair_separated_int(): @st.composite def separated_sum(draw): n1 = draw(st.integers(0, 1000)) draw(st.text()) draw(st.booleans()) draw(st.integers()) n2 = draw(st.integers(0, 1000)) return (n1, n2) assert minimal(separated_sum(), lambda x: sum(x) > 1000) == (1, 1000) def test_sum_of_pair_separated_float(): @st.composite def separated_sum(draw): f1 = draw(st.floats(0, 1000)) draw(st.text()) draw(st.booleans()) draw(st.integers()) f2 = draw(st.floats(0, 1000)) return (f1, f2) assert minimal(separated_sum(), lambda x: sum(x) > 1000) == (1, 1000) def test_calculator_benchmark(): """This test comes from https://github.com/jlink/shrinking-challenge/blob/main/challenges/calculator.md, which is originally from Pike, Lee. "SmartCheck: automatic and efficient counterexample reduction and generalization." Proceedings of the 2014 ACM SIGPLAN symposium on Haskell. 2014. """ expression = st.deferred( lambda: st.one_of( st.integers(), st.tuples(st.just("+"), expression, expression), st.tuples(st.just("/"), expression, expression), ) ) def div_subterms(e): if isinstance(e, int): return True if e[0] == "/" and e[-1] == 0: return False return div_subterms(e[1]) and div_subterms(e[2]) def evaluate(e): if isinstance(e, int): return e elif e[0] == "+": return evaluate(e[1]) + evaluate(e[2]) else: assert e[0] == "/" return evaluate(e[1]) // evaluate(e[2]) def is_failing(e): assume(div_subterms(e)) try: evaluate(e) return False except ZeroDivisionError: return True x = minimal(expression, is_failing) assert x == ("/", 0, ("+", 0, 0)) def test_one_of_slip(): assert minimal(st.integers(101, 200) | st.integers(0, 100)) == 101 # this limit is only to avoid Unsatisfiable when searching for an initial # counterexample in minimal, as we may generate a very large magnitude n. @given(st.integers(-(2**32), 2**32)) @settings(max_examples=3, suppress_health_check=[HealthCheck.nested_given]) def test_perfectly_shrinks_integers(n): if n >= 0: assert minimal(st.integers(), lambda x: x >= n) == n else: assert minimal(st.integers(), lambda x: x <= n) == n @given(st.integers(0, 20)) @settings(suppress_health_check=[HealthCheck.nested_given]) def test_lowering_together_positive(gap): s = st.tuples(st.integers(0, 20), st.integers(0, 20)) assert minimal(s, lambda x: x[0] + gap == x[1]) == (0, gap) @given(st.integers(-20, 0)) @settings(suppress_health_check=[HealthCheck.nested_given]) def test_lowering_together_negative(gap): s = st.tuples(st.integers(-20, 0), st.integers(-20, 0)) assert minimal(s, lambda x: x[0] + gap == x[1]) == (0, gap) @given(st.integers(-10, 10)) @settings(suppress_health_check=[HealthCheck.nested_given]) def test_lowering_together_mixed(gap): s = st.tuples(st.integers(-10, 10), st.integers(-10, 10)) assert minimal(s, lambda x: x[0] + gap == x[1]) == (0, gap) @given(st.integers(-10, 10)) @settings(suppress_health_check=[HealthCheck.nested_given]) def test_lowering_together_with_gap(gap): s = st.tuples(st.integers(-10, 10), st.text(), st.floats(), st.integers(-10, 10)) assert minimal(s, lambda x: x[0] + gap == x[3]) == (0, "", 0.0, gap) def test_run_length_encoding(): # extracted from https://github.com/HypothesisWorks/hypothesis/issues/4286, # as well as our docs def decode(table: list[tuple[int, str]]) -> str: out = "" for count, char in table: out += count * char return out def encode(s: str) -> list[tuple[int, str]]: count = 1 prev = "" out = [] if not s: return [] for char in s: if char != prev: if prev: entry = (count, prev) out.append(entry) # BUG: # count = 1 prev = char else: count += 1 entry = (count, char) out.append(entry) return out assert minimal(st.text(), lambda s: decode(encode(s)) != s) == "001" def test_minimize_duplicated_characters_within_a_choice(): # look for strings which have at least 3 of the same character, and also at # least two different characters (to avoid the trivial shrink of replacing # everything with "0" from working). # we should test this for st.binary too, but it's difficult to get it # to satisfy this precondition in the first place (probably worth improving # our generation here to duplicate binary elements in generate_mutations_from) assert ( minimal( st.text(min_size=1), lambda v: Counter(v).most_common()[0][1] > 2 and len(set(v)) > 1, ) == "0001" ) def test_nasty_string_shrinks(): # failures found via NASTY_STRINGS should shrink like normal assert ( minimal(st.text(), lambda s: "𝕿𝖍𝖊" in s, settings=settings(max_examples=10000)) == "𝕿𝖍𝖊" ) def test_bound5(): # redistribute_numeric_pairs should work for negative integers too bounded_ints = st.lists(st.integers(-100, 0), max_size=1) s = st.tuples( bounded_ints, bounded_ints, bounded_ints, bounded_ints, bounded_ints, ) assert minimal(s, lambda v: sum(sum(v, []), 0) < -150) == ( [], [], [], [-51], [-100], ) ================================================ FILE: hypothesis-python/tests/quality/test_zig_zagging.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from math import log from random import Random from hypothesis import ( HealthCheck, Phase, Verbosity, assume, example, given, settings, strategies as st, ) from hypothesis.internal.compat import ceil from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.engine import ConjectureRunner from tests.conjecture.common import interesting_origin @st.composite def problems(draw): m = draw(st.integers(256, 2**64 - 1)) assume(m > 0) marker = draw(st.binary(max_size=8)) bound = draw(st.integers(0, m - 1)) return (m, marker, bound) base_settings = settings( database=None, deadline=None, suppress_health_check=list(HealthCheck), max_examples=10, verbosity=Verbosity.normal, phases=(Phase.explicit, Phase.generate), ) @example((4503599627370496, b"", 2861143707951135)) @example((88305152, b"%\x1b\xa0\xfa", 12394667)) @example((99742672384, b"\xf5|", 24300326997)) @example((1454610481571840, b"", 1076887621690235)) @example((15616, b"", 2508)) @example((65536, b"", 20048)) @example((256, b"", 0)) @example((512, b"", 258)) @example((2048, b"", 1792)) @example((3072, b"", 0)) @example((256, b"", 1)) @settings( base_settings, verbosity=Verbosity.normal, phases=( # We disable shrinking for this test because when it fails it's a sign # that the shrinker is working really badly, so it ends up very slow! Phase.explicit, Phase.generate, ), max_examples=20, ) @given(problems()) def test_avoids_zig_zag_trap(p): m, marker, lower_bound = p n_bits = m.bit_length() + 1 def test_function(data): m = data.draw_integer(0, 2**n_bits - 1) if m < lower_bound: data.mark_invalid() n = data.draw_integer(0, 2**n_bits - 1) if data.draw_bytes(len(marker), len(marker)) != marker: data.mark_invalid() if abs(m - n) == 1: data.mark_interesting(interesting_origin()) runner = ConjectureRunner( test_function, database_key=None, settings=settings(base_settings, phases=(Phase.generate, Phase.shrink)), random=Random(0), ) runner.cached_test_function((m, m + 1, marker)) assert runner.interesting_examples runner.run() (v,) = runner.interesting_examples.values() data = ConjectureData.for_choices(v.choices) m = data.draw_integer(0, 2**n_bits - 1) n = data.draw_integer(0, 2**n_bits - 1) assert m == lower_bound if m == 0: assert n == 1 else: assert n == m - 1 budget = 2 * n_bits * ceil(log(n_bits, 2)) + 2 assert runner.shrinks <= budget ================================================ FILE: hypothesis-python/tests/redis/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/redis/test_redis_exampledatabase.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import uuid import pytest from fakeredis import FakeRedis from hypothesis import settings, strategies as st from hypothesis.database import InMemoryExampleDatabase from hypothesis.errors import InvalidArgument from hypothesis.extra.redis import RedisExampleDatabase from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule from tests.cover.test_database_backend import _database_conforms_to_listener_api @pytest.mark.parametrize( "kw", [ {"redis": "not a redis instance"}, {"redis": FakeRedis(), "expire_after": 10}, # not a timedelta {"redis": FakeRedis(), "key_prefix": "not a bytestring"}, {"redis": FakeRedis(), "listener_channel": 2}, # not a str ], ) def test_invalid_args_raise(kw): with pytest.raises(InvalidArgument): RedisExampleDatabase(**kw) def test_all_methods(): db = RedisExampleDatabase(FakeRedis()) db.save(b"key1", b"value") assert list(db.fetch(b"key1")) == [b"value"] db.move(b"key1", b"key2", b"value") assert list(db.fetch(b"key1")) == [] assert list(db.fetch(b"key2")) == [b"value"] db.delete(b"key2", b"value") assert list(db.fetch(b"key2")) == [] db.delete(b"key2", b"unknown value") class DatabaseComparison(RuleBasedStateMachine): def __init__(self): super().__init__() server = FakeRedis(host=uuid.uuid4().hex) # Different (fake) server each time self.dbs = [InMemoryExampleDatabase(), RedisExampleDatabase(server)] keys = Bundle("keys") values = Bundle("values") @rule(target=keys, k=st.binary()) def k(self, k): return k @rule(target=values, v=st.binary()) def v(self, v): return v @rule(k=keys, v=values) def save(self, k, v): for db in self.dbs: db.save(k, v) @rule(k=keys, v=values) def delete(self, k, v): for db in self.dbs: db.delete(k, v) @rule(k1=keys, k2=keys, v=values) def move(self, k1, k2, v): for db in self.dbs: db.move(k1, k2, v) @rule(k=keys) def values_agree(self, k): last = None last_db = None for db in self.dbs: keys = set(db.fetch(k)) if last is not None: assert last == keys, (last_db, db) last = keys last_db = db TestDBs = DatabaseComparison.TestCase def flush_messages(db): # fake redis doesn't have the background polling for pubsub that an actual # redis server does, so we have to flush when we want them. if db._pubsub is None: return # arbitrarily high. for _ in range(100): db._pubsub.get_message() def test_redis_listener(): _database_conforms_to_listener_api( lambda _path: RedisExampleDatabase(FakeRedis()), flush=flush_messages, parent_settings=settings( max_examples=5, stateful_step_count=10, ), ) def test_redis_listener_explicit(): calls = 0 def listener(event): nonlocal calls calls += 1 redis = FakeRedis() db = RedisExampleDatabase(redis) db.add_listener(listener) db.save(b"a", b"a") flush_messages(db) assert calls == 1 db.remove_listener(listener) db.delete(b"a", b"a") db.save(b"a", b"b") flush_messages(db) assert calls == 1 db.add_listener(listener) db.delete(b"a", b"b") db.save(b"a", b"c") flush_messages(db) assert calls == 3 db.save(b"a", b"c") flush_messages(db) assert calls == 3 def test_redis_move_from_key_without_value(): # explicit covering test for: # * moving a value from a key without that value redis = FakeRedis() db = RedisExampleDatabase(redis) db.save(b"a", b"x") db.save(b"b", b"x") db.move(b"a", b"b", b"y") def test_redis_move_into_key_with_value(): # explicit covering test for: # * moving a value into a key with that value redis = FakeRedis() db = RedisExampleDatabase(redis) db.save(b"a", b"y") db.save(b"b", b"x") db.move(b"a", b"b", b"x") def test_redis_move_to_same_key(): # explicit covering test for: # * moving a value where src == dest redis = FakeRedis() db = RedisExampleDatabase(redis) db.move(b"a", b"a", b"x") assert list(db.fetch(b"a")) == [b"x"] def test_redis_equality(): redis = FakeRedis() assert RedisExampleDatabase(redis) == RedisExampleDatabase(redis) # FakeRedis() != FakeRedis(), not much we can do here assert RedisExampleDatabase(FakeRedis()) != RedisExampleDatabase(FakeRedis()) ================================================ FILE: hypothesis-python/tests/test_annotated_types.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re import sys from typing import Annotated import pytest from hypothesis import given, strategies as st from hypothesis.errors import HypothesisWarning, ResolutionFailed from hypothesis.strategies._internal.lazy import unwrap_strategies from hypothesis.strategies._internal.strategies import FilteredStrategy from hypothesis.strategies._internal.types import _get_constraints from tests.common.debug import assert_simple_property, check_can_generate_examples try: import annotated_types as at except ImportError: pytest.skip(allow_module_level=True) def test_strategy_priority_over_constraints(): expected_strategy = st.SearchStrategy() strategy = st.from_type(Annotated[int, expected_strategy, at.Gt(1)]) assert strategy is expected_strategy def test_invalid_annotated_type(): msg = re.escape("Did you mean `Annotated[str | int, 'dummy']`?") with pytest.raises(ResolutionFailed, match=f".*{msg}$"): check_can_generate_examples( st.from_type(Annotated[str, "dummy", Annotated[int, "dummy"]]) ) @pytest.mark.parametrize( "unsupported_constraints,message", [ ((at.Timezone(None),), "Ignoring unsupported Timezone(tz=None)"), ((at.MultipleOf(1),), "Ignoring unsupported MultipleOf(multiple_of=1)"), ( (at.Timezone(None), at.MultipleOf(1)), "Ignoring unsupported Timezone(tz=None), MultipleOf(multiple_of=1)", ), ], ) def test_unsupported_constraints(unsupported_constraints, message): if sys.version_info >= (3, 11): # This is the preferred format, but also a SyntaxError on Python <= 3.10 t = eval("Annotated[int, *unsupported_constraints]", globals(), locals()) else: t = Annotated.__class_getitem__((int, *unsupported_constraints)) with pytest.warns(HypothesisWarning, match=re.escape(message)): check_can_generate_examples(st.from_type(t)) @pytest.mark.parametrize( "annotated_type,expected_strategy_repr", [ (Annotated[int, at.Gt(1)], "integers(min_value=2)"), (Annotated[int, at.Ge(1)], "integers(min_value=1)"), (Annotated[int, at.Lt(1)], "integers(max_value=0)"), (Annotated[int, at.Le(1)], "integers(max_value=1)"), (Annotated[int, at.Interval(ge=1, le=3)], "integers(1, 3)"), (Annotated[int, at.Interval(ge=1), at.Ge(2)], "integers(min_value=2)"), ], ) def test_annotated_type_int(annotated_type, expected_strategy_repr): strategy = unwrap_strategies(st.from_type(annotated_type)) assert repr(strategy) == expected_strategy_repr def test_predicate_constraint(): def func(_): return True strategy = unwrap_strategies(st.from_type(Annotated[int, at.Predicate(func)])) assert isinstance(strategy, FilteredStrategy) assert strategy.flat_conditions == (func,) class MyCollection: def __init__(self, values: list[int]) -> None: self._values = values def __len__(self) -> int: return len(self._values) @pytest.mark.parametrize("lo", [0, 1]) @pytest.mark.parametrize("hi", [None, 10]) @pytest.mark.parametrize("type_", [list[int], set[int], MyCollection]) @given(data=st.data()) def test_collection_sizes(data, lo, hi, type_): print(f"{type_=} {lo=} {hi=}") assert lo < (hi or 11) t = Annotated[type_, at.Len(min_length=lo, max_length=hi)] s = st.from_type(t) value = data.draw(s) assert lo is None or lo <= len(value) assert hi is None or len(value) <= hi @given(st.data()) def test_collection_size_from_slice(data): t = Annotated[MyCollection, "we just ignore this", slice(1, 10)] value = data.draw(st.from_type(t)) assert 1 <= len(value) <= 10 def test_unhashable_annotated_metadata(): t = Annotated[int, {"key": "value"}] assert_simple_property(st.from_type(t), lambda x: isinstance(x, int)) class GroupedStuff: __is_annotated_types_grouped_metadata__ = True def __init__(self, *args) -> None: self._args = args def __iter__(self): return iter(self._args) def __repr__(self) -> str: return f"GroupedStuff({', '.join(map(repr, self._args))})" def test_flattens_grouped_metadata(): grp = GroupedStuff(GroupedStuff(GroupedStuff(at.Len(min_length=1, max_length=5)))) constraints = list(_get_constraints(grp)) assert constraints == [at.MinLen(1), at.MaxLen(5)] try: # we can drop this ugly code when Python 3.10 reaches EOL from typing import NotRequired, TypedDict except ImportError: pass else: class TypedDictWithAnnotations(TypedDict): x: Annotated[int, at.Ge(0)] y: Annotated[NotRequired[int], at.Ge(0)] z: NotRequired[Annotated[int, at.Ge(0)]] @given(st.from_type(TypedDictWithAnnotations)) def test_typeddict_with_annotated_constraints(value): assert value["x"] >= 0 assert value.get("y", 0) >= 0 assert value.get("z", 0) >= 0 ================================================ FILE: hypothesis-python/tests/typing_extensions/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/typing_extensions/test_backported_types.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import collections from collections.abc import Callable from typing import Annotated, Concatenate, DefaultDict, NewType, TypeGuard, Union import pytest import typing_extensions from typing_extensions import ( LiteralString, NotRequired, ParamSpec, ReadOnly, Required, TypedDict, TypeIs, ) from hypothesis import HealthCheck, assume, given, settings, strategies as st from hypothesis.errors import InvalidArgument from hypothesis.strategies import from_type from hypothesis.strategies._internal.types import NON_RUNTIME_TYPES from tests.common.debug import ( assert_all_examples, assert_simple_property, check_can_generate_examples, find_any, ) # we'll continue testing the typing variants until their removal from the stdlib # ruff: noqa: UP035, UP006, UP007 # See also nocover/test_type_lookup.py @pytest.mark.parametrize("value", ["dog", b"goldfish", 42, 63.4, -80.5, False]) def test_typing_extensions_Literal(value): assert_simple_property( from_type(typing_extensions.Literal[value]), lambda v: v == value ) @given(st.data()) def test_typing_extensions_Literal_nested(data): lit = typing_extensions.Literal values = [ (lit["hamster", 0], ("hamster", 0)), (lit[26, False, "bunny", 130], (26, False, "bunny", 130)), (lit[lit[1]], {1}), (lit[lit[1], 2], {1, 2}), (lit[1, lit[2], 3], {1, 2, 3}), (lit[lit[lit[1], lit[2]], lit[lit[3], lit[4]]], {1, 2, 3, 4}), # See https://github.com/HypothesisWorks/hypothesis/pull/2886 (Union[lit["hamster"], lit["bunny"]], {"hamster", "bunny"}), # noqa (Union[lit[lit[1], lit[2]], lit[lit[3], lit[4]]], {1, 2, 3, 4}), ] literal_type, flattened_literals = data.draw(st.sampled_from(values)) assert data.draw(st.from_type(literal_type)) in flattened_literals class A(TypedDict): a: int @given(from_type(A)) def test_simple_typeddict(value): assert type(value) == dict assert set(value) == {"a"} assert isinstance(value["a"], int) def test_typing_extensions_Type_int(): assert_simple_property(from_type(type[int]), lambda v: v is int) @given(from_type(Union[type[str], type[list]])) def test_typing_extensions_Type_Union(ex): assert ex in (str, list) def test_resolves_NewType(): typ = NewType("T", int) nested = NewType("NestedT", typ) uni = NewType("UnionT", Union[int, None]) uni_new = NewType("UnionT", int | None) assert_simple_property(from_type(typ), lambda x: isinstance(x, int)) assert_simple_property(from_type(nested), lambda x: isinstance(x, int)) assert_simple_property(from_type(uni), lambda x: isinstance(x, (int, type(None)))) assert_simple_property( from_type(uni_new), lambda x: isinstance(x, (int, type(None))) ) find_any(from_type(uni), lambda x: isinstance(x, int)) find_any(from_type(uni), lambda x: isinstance(x, type(None))) @given(from_type(DefaultDict[int, int]) | from_type(collections.defaultdict[int, int])) def test_defaultdict(ex): assert isinstance(ex, collections.defaultdict) assume(ex) assert all(isinstance(elem, int) for elem in ex) assert all(isinstance(elem, int) for elem in ex.values()) @pytest.mark.parametrize("non_runtime_type", NON_RUNTIME_TYPES) def test_non_runtime_type_cannot_be_resolved(non_runtime_type): strategy = st.from_type(non_runtime_type) with pytest.raises( InvalidArgument, match="there is no such thing as a runtime instance" ): check_can_generate_examples(strategy) @pytest.mark.parametrize("non_runtime_type", NON_RUNTIME_TYPES) def test_non_runtime_type_cannot_be_registered(non_runtime_type): with pytest.raises( InvalidArgument, match="there is no such thing as a runtime instance" ): st.register_type_strategy(non_runtime_type, st.none()) def test_callable_with_concatenate(): P = ParamSpec("P") func_type = Callable[Concatenate[int, P], None] strategy = st.from_type(func_type) with pytest.raises( InvalidArgument, match="Hypothesis can't yet construct a strategy for instances of a Callable type", ): check_can_generate_examples(strategy) with pytest.raises(InvalidArgument, match="Cannot register generic type"): st.register_type_strategy(func_type, st.none()) def test_callable_with_paramspec(): P = ParamSpec("P") func_type = Callable[P, None] strategy = st.from_type(func_type) with pytest.raises( InvalidArgument, match="Hypothesis can't yet construct a strategy for instances of a Callable type", ): check_can_generate_examples(strategy) with pytest.raises(InvalidArgument, match="Cannot register generic type"): st.register_type_strategy(func_type, st.none()) @pytest.mark.parametrize("typ", [TypeGuard, TypeIs]) def test_callable_return_typegard_type(typ): strategy = st.from_type(Callable[[], typ[int]]) with pytest.raises( InvalidArgument, match="Hypothesis cannot yet construct a strategy for callables " "which are PEP-647 TypeGuards or PEP-742 TypeIs", ): check_can_generate_examples(strategy) with pytest.raises(InvalidArgument, match="Cannot register generic type"): st.register_type_strategy(Callable[[], typ[int]], st.none()) class Movie(TypedDict): # implicitly total=True title: str year: NotRequired[int] @given(from_type(Movie)) def test_typeddict_not_required(value): assert type(value) == dict assert set(value).issubset({"title", "year"}) assert isinstance(value["title"], str) if "year" in value: assert isinstance(value["year"], int) def test_typeddict_not_required_can_skip(): find_any(from_type(Movie), lambda movie: "year" not in movie) class OtherMovie(TypedDict, total=False): title: Required[str] year: int @given(from_type(OtherMovie)) def test_typeddict_required(value): assert type(value) == dict assert set(value).issubset({"title", "year"}) assert isinstance(value["title"], str) if "year" in value: assert isinstance(value["year"], int) def test_typeddict_required_must_have(): assert_all_examples(from_type(OtherMovie), lambda movie: "title" in movie) class Story(TypedDict, total=True): author: str class Book(Story, total=False): pages: int class Novel(Book): genre: Required[str] rating: NotRequired[str] @pytest.mark.parametrize( "check,condition", [ pytest.param( assert_all_examples, lambda novel: "author" in novel, id="author-is-required", ), pytest.param( assert_all_examples, lambda novel: "genre" in novel, id="genre-is-required" ), pytest.param( find_any, lambda novel: "pages" in novel, id="pages-may-be-present" ), pytest.param( find_any, lambda novel: "pages" not in novel, id="pages-may-be-absent" ), pytest.param( find_any, lambda novel: "rating" in novel, id="rating-may-be-present" ), pytest.param( find_any, lambda novel: "rating" not in novel, id="rating-may-be-absent" ), ], ) def test_required_and_not_required_keys(check, condition): check(from_type(Novel), condition) class DeeplyNestedQualifiers(TypedDict): a: ReadOnly[Required[int]] b: NotRequired[Annotated[ReadOnly[int], "metadata"]] c: Annotated[ReadOnly[NotRequired[str]], "metadata"] @pytest.mark.parametrize( "check,condition", [ pytest.param( assert_all_examples, lambda novel: "a" in novel, id="a-is-required", ), pytest.param(find_any, lambda novel: "b" in novel, id="b-may-be-present"), pytest.param(find_any, lambda novel: "b" not in novel, id="b-may-be-absent"), pytest.param(find_any, lambda novel: "c" in novel, id="c-may-be-present"), pytest.param(find_any, lambda novel: "c" not in novel, id="c-may-be-absent"), ], ) def test_required_and_not_required_keys_deeply_nested(check, condition): check(from_type(DeeplyNestedQualifiers), condition) def test_typeddict_error_msg(): with pytest.raises(TypeError, match="is not valid as type argument"): class Foo(TypedDict): attr: Required with pytest.raises(TypeError, match="is not valid as type argument"): class Bar(TypedDict): attr: NotRequired with pytest.raises(TypeError, match="is not valid as type argument"): class Baz(TypedDict): attr: ReadOnly def test_literal_string_is_just_a_string(): assert_all_examples(from_type(LiteralString), lambda thing: isinstance(thing, str)) class Foo: def __init__(self, x): pass class Bar(Foo): pass class Baz(Foo): pass st.register_type_strategy(Bar, st.builds(Bar, st.integers())) st.register_type_strategy(Baz, st.builds(Baz, st.integers())) T = typing_extensions.TypeVar("T") T_int = typing_extensions.TypeVar("T_int", bound=int) @pytest.mark.parametrize( "var,expected", [ (typing_extensions.TypeVar("V"), object), # Bound: (typing_extensions.TypeVar("V", bound=int), int), (typing_extensions.TypeVar("V", bound=Foo), (Bar, Baz)), (typing_extensions.TypeVar("V", bound=Union[int, str]), (int, str)), (typing_extensions.TypeVar("V", bound=int | str), (int, str)), # Constraints: (typing_extensions.TypeVar("V", int, str), (int, str)), # Default: (typing_extensions.TypeVar("V", default=int), int), (typing_extensions.TypeVar("V", default=T), object), (typing_extensions.TypeVar("V", default=Foo), (Bar, Baz)), (typing_extensions.TypeVar("V", default=Union[int, str]), (int, str)), (typing_extensions.TypeVar("V", default=int | str), (int, str)), (typing_extensions.TypeVar("V", default=T_int), int), (typing_extensions.TypeVar("V", default=T_int, bound=int), int), (typing_extensions.TypeVar("V", int, str, default=int), (int, str)), # This case is not correct from typing's perspective, but its not # our job to very this, static type-checkers should do that: (typing_extensions.TypeVar("V", default=T_int, bound=str), (int, str)), ], ) @settings(suppress_health_check=[HealthCheck.too_slow]) @given(data=st.data()) def test_typevar_type_is_consistent(data, var, expected): strat = st.from_type(var) v1 = data.draw(strat) v2 = data.draw(strat) assume(v1 != v2) # Values may vary, just not types assert type(v1) == type(v2) assert isinstance(v1, expected) ================================================ FILE: hypothesis-python/tests/watchdog/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: hypothesis-python/tests/watchdog/test_database.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import sys import time from collections import Counter import pytest from hypothesis import Phase, settings from hypothesis.database import ( DirectoryBasedExampleDatabase, InMemoryExampleDatabase, MultiplexedDatabase, ) from tests.common.utils import flaky, skipif_threading, wait_for from tests.cover.test_database_backend import _database_conforms_to_listener_api # Our database listener tests are tantalizingly close to being useful, but are # still subtly flaky. I have spent two afternoons trying to isolate the problem # and have concluded it would require sitting down at a deep level with not only # the watchdog implementation but also platform-specific filesystem watcher details. # # I've seen issues like duplicate events being emitted, stopping/starting a listener # in quick succession repeating the previous batch of events, and other weirdness. # # These tests are still useful as a manual check when changing database listener # code, and as an encoding of our desired semantics. pytestmark = pytest.mark.skip(reason="see comment") # we need real time here, not monkeypatched for CI time_sleep = time.sleep def test_database_listener_directory(): _database_conforms_to_listener_api( lambda path: DirectoryBasedExampleDatabase(path), supports_value_delete=False, parent_settings=settings( # this test is very expensive because we wait between every rule for # the filesystem observer to fire. max_examples=5, stateful_step_count=10, # expensive runtime makes shrinking take forever phases=set(Phase) - {Phase.shrink}, deadline=None, ), ) # seen flaky on test-win; we get *three* of the same save events in the first # assertion, which...is baffling, and possibly a genuine bug (most likely in # watchdog). @skipif_threading # add_listener is not thread safe because watchdog is not def test_database_listener_multiplexed(tmp_path): db = MultiplexedDatabase( InMemoryExampleDatabase(), DirectoryBasedExampleDatabase(tmp_path) ) events = [] def listener(event): events.append(event) db.add_listener(listener) time_sleep(0.1) db.save(b"a", b"a") wait_for(lambda: events == [("save", (b"a", b"a"))] * 2, timeout=30) db.remove_listener(listener) db.delete(b"a", b"a") db.save(b"a", b"b") wait_for(lambda: events == [("save", (b"a", b"a"))] * 2, timeout=30) db.add_listener(listener) time_sleep(0.1) db.delete(b"a", b"b") db.save(b"a", b"c") # InMemory database fires immediately, while DirectoryBased has to # wait for filesystem listeners. Therefore the events can arrive out of # order. Test a weaker multiset property, disregarding ordering. wait_for( lambda: Counter(events[2:]) == { # InMemory ("delete", (b"a", b"b")): 1, # DirectoryBased ("delete", (b"a", None)): 1, # both ("save", (b"a", b"c")): 2, }, timeout=30, ) @skipif_threading # add_listener is not thread safe because watchdog is not def test_database_listener_directory_explicit(tmp_path): db = DirectoryBasedExampleDatabase(tmp_path) events = [] def listener(event): events.append(event) time_sleep(0.1) db.add_listener(listener) time_sleep(0.1) db.save(b"k1", b"v1") wait_for(lambda: events == [("save", (b"k1", b"v1"))], timeout=30) time_sleep(0.1) db.remove_listener(listener) time_sleep(0.1) db.delete(b"k1", b"v1") db.save(b"k1", b"v2") wait_for(lambda: events == [("save", (b"k1", b"v1"))], timeout=30) time_sleep(0.1) db.add_listener(listener) time_sleep(0.1) db.delete(b"k1", b"v2") db.save(b"k1", b"v3") wait_for( lambda: events[1:] == [ ("delete", (b"k1", None)), ("save", (b"k1", b"v3")), ], timeout=30, ) # moving into a nonexistent key db.move(b"k1", b"k2", b"v3") time_sleep(0.5) # moving back into an existing key db.move(b"k2", b"k1", b"v3") time_sleep(0.5) if sys.platform.startswith("darwin"): assert events[3:] == [ ("delete", (b"k1", b"v3")), ("save", (b"k2", b"v3")), ("delete", (b"k2", b"v3")), ("save", (b"k1", b"v3")), ], str(events[3:]) elif sys.platform.startswith("win"): # watchdog fires save/delete events instead of move events on windows. # This means we don't broadcast the exact deleted value. assert events[3:] == [ ("delete", (b"k1", None)), ("save", (b"k2", b"v3")), ("delete", (b"k2", None)), ("save", (b"k1", b"v3")), ], str(events[3:]) elif sys.platform.startswith("linux"): # move #1 assert ("save", (b"k2", b"v3")) in events # sometimes watchdog fires a move event (= save + delete with value), # and other times it fires separate save and delete events (= delete with # no value). I think this is due to particulars of what happens when # a new directory gets created very close to the time when a file is # saved to that directory. assert any(("delete", (b"k1", val)) in events for val in [b"v3", None]) # move #2 assert ("save", (b"k1", b"v3")) in events assert any(("delete", (b"k2", val)) in events for val in [b"v3", None]) else: raise NotImplementedError(f"unknown platform {sys.platform}") @flaky(max_runs=5, min_passes=1) # time_sleep(0.1) probably isn't enough here @skipif_threading # add_listener is not thread safe because watchdog is not def test_database_listener_directory_move(tmp_path): db = DirectoryBasedExampleDatabase(tmp_path) events = [] def listener(event): events.append(event) # make sure both keys exist and that v1 exists in k1 and not k2 db.save(b"k1", b"v1") db.save(b"k2", b"v_unrelated") time_sleep(0.1) db.add_listener(listener) time_sleep(0.1) db.move(b"k1", b"k2", b"v1") # events might arrive in either order wait_for( lambda: set(events) == { ("save", (b"k2", b"v1")), # windows doesn't fire move events, so value is None ("delete", (b"k1", None if sys.platform.startswith("win") else b"v1")), }, timeout=30, ) @skipif_threading # add_listener is not thread safe because watchdog is not def test_still_listens_if_directory_did_not_exist(tmp_path): # if we start listening on a nonexistent path, we will create that path and # still listen for events events = [] def listener(event): events.append(event) p = tmp_path / "does_not_exist_yet" db = DirectoryBasedExampleDatabase(p) assert not p.exists() db.add_listener(listener) assert p.exists() assert not events db.save(b"k1", b"v1") wait_for(lambda: len(events) == 1, timeout=30) ================================================ FILE: hypothesis-python/tests/watchdog/test_database_cover.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.database import ( DirectoryBasedExampleDatabase, InMemoryExampleDatabase, MultiplexedDatabase, ) from tests.common.utils import skipif_threading # trivial covering tests as a stopgap while we skip our proper database listener # tests. Can be removed when we re-enable those. @skipif_threading def test_start_stop_multiplexed_listener(tmp_path): db = MultiplexedDatabase( InMemoryExampleDatabase(), DirectoryBasedExampleDatabase(tmp_path) ) listener = lambda event: None db.add_listener(listener) db.remove_listener(listener) @skipif_threading def test_start_stop_directory_listener(tmp_path): db = DirectoryBasedExampleDatabase(tmp_path) listener = lambda event: None db.add_listener(listener) db.remove_listener(listener) ================================================ FILE: hypothesis-python/tox.ini ================================================ [tox] envlist = py{310,py310,311,py311,312,313,313t,314,314t,315,315t}-{brief,full,cover,rest,nocover,niche,custom} toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] basepython={env:TOX_PYTHON_VERSION:python3} deps = -r../requirements/test.txt extras = zoneinfo allowlist_externals = bash passenv= HOME LC_ALL COVERAGE_FILE TOXENV # Allow CI builds (or user builds) to force coloured terminal output. PY_COLORS setenv = # If PY...ING is set in calling environment, use that; default to 1 # If PY...ING is set in calling environment, use that; default to 1 PYTHONWARNDEFAULTENCODING={env:PYTHONWARNDEFAULTENCODING:1} brief: HYPOTHESIS_PROFILE=speedy commands = full: bash scripts/basic-test.sh brief: python -bb -X dev -m pytest -n auto tests/cover/test_testdecorators.py {posargs} cover: python -bb -X dev -m pytest -n auto tests/cover/ tests/pytest/ tests/conjecture/ {posargs} rest: python -bb -X dev -m pytest -n auto tests/ --ignore=tests/cover/ --ignore=tests/pytest/ --ignore=tests/conjecture/ --ignore=tests/nocover/ --ignore=tests/quality/ --ignore=tests/ghostwriter/ --ignore=tests/patching/ --ignore=tests/crosshair/test_crosshair.py {posargs} conjecture: python -bb -X dev -m pytest -n auto tests/conjecture/ {posargs} nocover: python -bb -X dev -m pytest -n auto tests/nocover/ {posargs} niche: bash scripts/other-tests.sh custom: python -bb -X dev -m pytest {posargs} [testenv:py310-pyjion] deps = -r../requirements/test.txt # We'd like to pin this, but pip-compile has to run under Python 3.10 (+) # to do so and that's painful because other tools aren't compatible yet. # If it's mid-2022, try again, starting by updating ci_version to 3.10 pyjion commands = # TODO: restore `-n auto` https://github.com/tonybaloney/Pyjion/issues/456 # TODO: re-enable in Actions main.yml once this actually works pyjion -m pytest tests/cover tests/pytest tests/nocover [testenv:quality] deps= -r../requirements/test.txt commands= python -bb -X dev -m pytest tests/quality/ -n auto # This test job runs on the oldest version of CPython we support, against the minimum # version specified in our runtime dependencies. For now, that's the oldest version # with a wheel for non-EOL Python. In future we might deprecate faster per NEP-29. [testenv:py310-oldestnumpy] deps= -r../requirements/test.txt allowlist_externals = bash commands_pre = bash -c "pip install --only-binary=:all: numpy==$(grep -m 1 'numpy>=' pyproject.toml | grep -oE '[0-9.]+')" commands = python -bb -X dev -m pytest tests/numpy/ -n auto # This test job runs against the nightly version of `numpy` [testenv:numpy-nightly] deps= -r../requirements/test.txt pandas black click attrs # ghostwriter has an attrs test allowlist_externals = bash commands_pre = bash -c "pip install --upgrade --pre --only-binary :all: -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy" commands = python -bb -X dev -m pytest tests/numpy/ tests/array_api/ tests/pandas/ tests/ghostwriter/ tests/conjecture/ tests/cover -n auto {posargs} # Note: when adding or removing tested Pandas versions, make sure to update the # docs in numpy.rst too. To see current download rates of each minor version: # https://pepy.tech/project/pandas?versions=1.1.*&versions=1.2.*&versions=1.3.*&versions=1.4.*&versions=1.5.*&versions=2.0.*&versions=2.1.*&versions=2.2.* [testenv:py310-pandas11] deps = -r../requirements/test.txt numpy~=1.26.4 pandas~=1.1.5 commands = python -bb -X dev -m pytest tests/pandas -n auto [testenv:py310-pandas12] deps = -r../requirements/test.txt numpy~=1.26.4 pandas~=1.2.5 commands = python -bb -X dev -m pytest tests/pandas -n auto [testenv:py310-pandas13] deps = -r../requirements/test.txt numpy~=1.26.4 pandas~=1.3.5 commands = python -bb -X dev -m pytest tests/pandas -n auto [testenv:py310-pandas14] deps = -r../requirements/test.txt numpy~=1.26.4 pandas~=1.4.4 commands = python -bb -X dev -m pytest tests/pandas -n auto [testenv:py311-pandas15] deps = -r../requirements/test.txt numpy~=1.26.4 pandas~=1.5.3 commands = python -bb -X dev -m pytest tests/pandas -n auto [testenv:py311-pandas20] deps = -r../requirements/test.txt numpy~=1.26.4 pandas~=2.0.3 commands = python -bb -X dev -m pytest tests/pandas -n auto [testenv:py312-pandas21] deps = -r../requirements/test.txt numpy~=1.26.4 pandas~=2.1.4 setenv= PYTHONWARNDEFAULTENCODING=1 commands = python -bb -X dev -m pytest tests/pandas -n auto [testenv:py313-pandas22] deps = -r../requirements/test.txt pandas~=2.2.2 # https://pandas.pydata.org/docs/whatsnew/v2.2.2.html#pandas-2-2-2-is-now-compatible-with-numpy-2-0 setenv= PYTHONWARNDEFAULTENCODING=1 commands = python -bb -X dev -m pytest tests/pandas -n auto # Adding a new pandas? See comment above! [testenv:rest] deps = -r../requirements/coverage.txt -r../requirements/crosshair.txt [testenv:alt-{rest,nocover}] deps = -r../requirements/test.txt -r../requirements/crosshair.txt # for conformance test numpy==1.26.4 pandas==2.0.3 # latest available as x86 binary wheel allowlist_externals = bash commands_pre = bash -c "grep -v 'numpy=\|pandas=\|pyarrow=' ../requirements/coverage.txt >coverage-mod.txt" pip install -r coverage-mod.txt [testenv:crosshair-{cover,nocover,niche,custom}] deps = -r../requirements/test.txt -r../requirements/crosshair.txt allowlist_externals = bash setenv= HYPOTHESIS_PROFILE=crosshair commands = # invoke with `./build.sh check-crosshair-cover -- -x -Wignore` cover: python -bb -X dev -m pytest -n auto tests/cover/ tests/pytest/ {posargs} nocover: python -bb -X dev -m pytest -n auto tests/nocover/ {posargs} niche: bash scripts/other-tests.sh custom: python -bb -X dev -m pytest {posargs} [testenv:django{42,52,60,-nocontrib}] setenv= PYTHONWARNDEFAULTENCODING=1 DJANGO_SETTINGS_MODULE=tests.django.toys.settings.settings nocontrib: DJANGO_SETTINGS_MODULE=tests.django.toys.settings.settings_no_contrib deps= -r../requirements/test.txt django42: django==4.2.29 django52: django==5.2.12 django60: django==6.0.3 django-nocontrib: django==6.0.3 commands = python -bb -X dev -m tests.django.manage test tests.django {posargs} [testenv:py{39}-pytest54] deps = -r../requirements/test.txt commands_pre = pip install pytest==5.4.3 pytest-xdist commands = python -bb -X dev -m pytest tests/pytest tests/cover/test_testdecorators.py [testenv:pytest62] deps = -r../requirements/test.txt setenv= PYTHONWARNDEFAULTENCODING= commands_pre = pip install pytest==6.2.5 pytest-xdist commands = python -bb -X dev -m pytest tests/pytest tests/cover/test_testdecorators.py [testenv:pytest74] deps = -r../requirements/test.txt commands_pre = pip install pytest==7.4.4 pytest-xdist commands = python -bb -X dev -m pytest tests/pytest tests/cover/test_testdecorators.py [testenv:pytest84] deps = -r../requirements/test.txt commands_pre = pip install pytest==8.4.2 pytest-xdist commands = python -bb -X dev -m pytest tests/pytest tests/cover/test_testdecorators.py [testenv:pytest9] deps = -r../requirements/test.txt commands_pre = pip install pytest==9.* pytest-xdist commands = python -bb -X dev -m pytest tests/pytest tests/cover/test_testdecorators.py [testenv:threading] deps = -r../requirements/test.txt # in pytest-run-parallel==0.7.0, python3.10 + pytest-run-parallel + xdist does # not collect any tests. I don't know why. pytest-run-parallel==0.6.0 setenv= PYTHONWARNDEFAULTENCODING=1 HYPOTHESIS_PROFILE=threading commands = python -bb -X dev -m pytest -n auto tests/cover --parallel-threads 3 --skip-thread-unsafe true {posargs} [testenv:coverage] deps = -r../requirements/coverage.txt allowlist_externals = rm setenv= PYTHONWARNDEFAULTENCODING=1 HYPOTHESIS_INTERNAL_COVERAGE=true COVERAGE_PROCESS_START=.coveragerc commands_pre = rm -f branch-check* pip install .[zoneinfo] pip install coverage_enable_subprocess commands_post = pip uninstall -y coverage_enable_subprocess # Produce a coverage report even if the test suite fails. # (The tox task will still count as failed.) ignore_errors = true # We've had problems correctly measuring coverage using pytest-cov when running # in parallel, so instead we start coverage implicitly on all (sub-)processes by # way of the coverage_enable_subprocesses installation. This requires all options # to be set in .coveragerc (including source), but that's ok as it is overridden # by --cov=... in conjecture-coverage. commands = python -bb -X dev -m pytest -n auto --ff {posargs} \ tests/cover tests/conjecture tests/datetime tests/numpy tests/pandas tests/lark \ tests/redis tests/dpcontracts tests/codemods tests/typing_extensions tests/patching \ tests/test_annotated_types.py tests/watchdog python -m coverage combine python -m coverage report python scripts/validate_branch_check.py [testenv:conjecture-coverage] deps = -r../requirements/coverage.txt setenv= PYTHONWARNDEFAULTENCODING=1 HYPOTHESIS_INTERNAL_COVERAGE=true commands = python -bb -X dev \ -m pytest -n auto tests/conjecture/ \ --cov=hypothesis.internal.conjecture --cov-config=.coveragerc {posargs} [testenv:examples3] deps= -r../requirements/test.txt commands_pre = python -m pip install --editable examples/example_hypothesis_entrypoint commands = python -bb -X dev -m pytest examples ================================================ FILE: notebooks/Designing a better simplifier.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Designing a better simplifier\n", "\n", "This is a notebook talking through some of the considerations in the design of Hypothesis's approach to simplification.\n", "\n", "It doesn't perfectly mirror what actually happens in Hypothesis, but it should give some consideration to the sort of things that Hypothesis does and why it takes a particular approach.\n", "\n", "In order to simplify the scope of this document we are only going to\n", "concern ourselves with lists of integers. There are a number of API considerations involved in expanding beyond that point, however most of the algorithmic considerations are the same.\n", "\n", "The big difference between lists of integers and the general case is that integers can never be too complex. In particular we will rapidly get to the point where individual elements can be simplified in usually only log(n) calls. When dealing with e.g. lists of lists this is a much more complicated proposition. That may be covered in another notebook.\n", "\n", "Our objective here is to minimize the number of times we check the condition. We won't be looking at actual timing performance, because usually the speed of the condition is the bottleneck there (and where it's not, everything is fast enough that we need not worry)." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def greedy_shrink(ls, constraint, shrink):\n", " \"\"\"\n", " This is the \"classic\" QuickCheck algorithm which takes a shrink function\n", " which will iterate over simpler versions of an example. We are trying\n", " to find a local minima: That is an example ls such that condition(ls)\n", " is True but that constraint(t) is False for each t in shrink(ls).\n", " \"\"\"\n", " while True:\n", " for s in shrink(ls):\n", " if constraint(s):\n", " ls = s\n", " break\n", " else:\n", " return ls" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def shrink1(ls):\n", " \"\"\"\n", " This is our prototype shrink function. It is very bad. It makes the\n", " mistake of only making very small changes to an example each time.\n", "\n", " Most people write something like this the first time they come to\n", " implement example shrinking. In particular early Hypothesis very much\n", " made this mistake.\n", "\n", " What this does:\n", "\n", " For each index, if the value of the index is non-zero we try\n", " decrementing it by 1.\n", "\n", " We then (regardless of if it's zero) try the list with the value at\n", " that index deleted.\n", " \"\"\"\n", " for i in range(len(ls)):\n", " s = list(ls)\n", " if s[i] > 0:\n", " s[i] -= 1\n", " yield list(s)\n", " del s[i]\n", " yield list(s)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def show_trace(start, constraint, simplifier):\n", " \"\"\"\n", " This is a debug function. You shouldn't concern yourself with\n", " its implementation too much.\n", "\n", " What it does is print out every intermediate step in applying a\n", " simplifier (a function of the form (list, constraint) -> list)\n", " along with whether it is a successful shrink or not.\n", " \"\"\"\n", " if start is None:\n", " while True:\n", " start = gen_list()\n", " if constraint(start):\n", " break\n", "\n", " shrinks = [0]\n", " tests = [0]\n", "\n", " def print_shrink(ls):\n", " tests[0] += 1\n", " if constraint(ls):\n", " shrinks[0] += 1\n", " print(\"✓\", ls)\n", " return True\n", " else:\n", " print(\"✗\", ls)\n", " return False\n", "\n", " print(\"✓\", start)\n", " simplifier(start, print_shrink)\n", " print()\n", " print(\"%d shrinks with %d function calls\" % (shrinks[0], tests[0]))" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from functools import partial" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [5, 5]\n", "✓ [4, 5]\n", "✓ [3, 5]\n", "✓ [2, 5]\n", "✓ [1, 5]\n", "✓ [0, 5]\n", "✗ [5]\n", "✓ [0, 4]\n", "✗ [4]\n", "✓ [0, 3]\n", "✗ [3]\n", "✓ [0, 2]\n", "✗ [2]\n", "✓ [0, 1]\n", "✗ [1]\n", "✓ [0, 0]\n", "✗ [0]\n", "✗ [0]\n", "\n", "10 shrinks with 17 function calls\n" ] } ], "source": [ "show_trace([5, 5], lambda x: len(x) >= 2, partial(greedy_shrink, shrink=shrink1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That worked reasonably well, but it sure was a lot of function calls for such a small amount of shrinking. What would have happened if we'd started with [100, 100]?" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def shrink2(ls):\n", " \"\"\"\n", " Here is an improved shrink function. We first try deleting each element\n", " and then we try making each element smaller, but we do so from the left\n", " hand side instead of the right. This means we will always find the\n", " smallest value that can go in there, but we will do so much sooner.\n", " \"\"\"\n", " for i in range(len(ls)):\n", " s = list(ls)\n", " del s[i]\n", " yield list(s)\n", "\n", " for i in range(len(ls)):\n", " for x in range(ls[i]):\n", " s = list(ls)\n", " s[i] = x\n", " yield s" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [5, 5]\n", "✗ [5]\n", "✗ [5]\n", "✓ [0, 5]\n", "✗ [5]\n", "✗ [0]\n", "✓ [0, 0]\n", "✗ [0]\n", "✗ [0]\n", "\n", "2 shrinks with 8 function calls\n" ] } ], "source": [ "show_trace([5, 5], lambda x: len(x) >= 2, partial(greedy_shrink, shrink=shrink2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This did indeed reduce the number of function calls significantly - we immediately determine that the value in the cell doesn't matter and we can just put zero there. \n", "\n", "But what would have happened if the value *did* matter?" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [1000]\n", "✗ []\n", "✗ [0]\n", "✗ [1]\n", "✗ [2]\n", "✗ [3]\n", "✗ [4]\n", "✗ [5]\n", "✗ [6]\n", "✗ [7]\n", "✗ [8]\n", "✗ [9]\n", "✗ [10]\n", "✗ [11]\n", "✗ [12]\n", "✗ [13]\n", "✗ [14]\n", "✗ [15]\n", "✗ [16]\n", "✗ [17]\n", "✗ [18]\n", "✗ [19]\n", "✗ [20]\n", "✗ [21]\n", "✗ [22]\n", "✗ [23]\n", "✗ [24]\n", "✗ [25]\n", "✗ [26]\n", "✗ [27]\n", "✗ [28]\n", "✗ [29]\n", "✗ [30]\n", "✗ [31]\n", "✗ [32]\n", "✗ [33]\n", "✗ [34]\n", "✗ [35]\n", "✗ [36]\n", "✗ [37]\n", "✗ [38]\n", "✗ [39]\n", "✗ [40]\n", "✗ [41]\n", "✗ [42]\n", "✗ [43]\n", "✗ [44]\n", "✗ [45]\n", "✗ [46]\n", "✗ [47]\n", "✗ [48]\n", "✗ [49]\n", "✗ [50]\n", "✗ [51]\n", "✗ [52]\n", "✗ [53]\n", "✗ [54]\n", "✗ [55]\n", "✗ [56]\n", "✗ [57]\n", "✗ [58]\n", "✗ [59]\n", "✗ [60]\n", "✗ [61]\n", "✗ [62]\n", "✗ [63]\n", "✗ [64]\n", "✗ [65]\n", "✗ [66]\n", "✗ [67]\n", "✗ [68]\n", "✗ [69]\n", "✗ [70]\n", "✗ [71]\n", "✗ [72]\n", "✗ [73]\n", "✗ [74]\n", "✗ [75]\n", "✗ [76]\n", "✗ [77]\n", "✗ [78]\n", "✗ [79]\n", "✗ [80]\n", "✗ [81]\n", "✗ [82]\n", "✗ [83]\n", "✗ [84]\n", "✗ [85]\n", "✗ [86]\n", "✗ [87]\n", "✗ [88]\n", "✗ [89]\n", "✗ [90]\n", "✗ [91]\n", "✗ [92]\n", "✗ [93]\n", "✗ [94]\n", "✗ [95]\n", "✗ [96]\n", "✗ [97]\n", "✗ [98]\n", "✗ [99]\n", "✗ [100]\n", "✗ [101]\n", "✗ [102]\n", "✗ [103]\n", "✗ [104]\n", "✗ [105]\n", "✗ [106]\n", "✗ [107]\n", "✗ [108]\n", "✗ [109]\n", "✗ [110]\n", "✗ [111]\n", "✗ [112]\n", "✗ [113]\n", "✗ [114]\n", "✗ [115]\n", "✗ [116]\n", "✗ [117]\n", "✗ [118]\n", "✗ [119]\n", "✗ [120]\n", "✗ [121]\n", "✗ [122]\n", "✗ [123]\n", "✗ [124]\n", "✗ [125]\n", "✗ [126]\n", "✗ [127]\n", "✗ [128]\n", "✗ [129]\n", "✗ [130]\n", "✗ [131]\n", "✗ [132]\n", "✗ [133]\n", "✗ [134]\n", "✗ [135]\n", "✗ [136]\n", "✗ [137]\n", "✗ [138]\n", "✗ [139]\n", "✗ [140]\n", "✗ [141]\n", "✗ [142]\n", "✗ [143]\n", "✗ [144]\n", "✗ [145]\n", "✗ [146]\n", "✗ [147]\n", "✗ [148]\n", "✗ [149]\n", "✗ [150]\n", "✗ [151]\n", "✗ [152]\n", "✗ [153]\n", "✗ [154]\n", "✗ [155]\n", "✗ [156]\n", "✗ [157]\n", "✗ [158]\n", "✗ [159]\n", "✗ [160]\n", "✗ [161]\n", "✗ [162]\n", "✗ [163]\n", "✗ [164]\n", "✗ [165]\n", "✗ [166]\n", "✗ [167]\n", "✗ [168]\n", "✗ [169]\n", "✗ [170]\n", "✗ [171]\n", "✗ [172]\n", "✗ [173]\n", "✗ [174]\n", "✗ [175]\n", "✗ [176]\n", "✗ [177]\n", "✗ [178]\n", "✗ [179]\n", "✗ [180]\n", "✗ [181]\n", "✗ [182]\n", "✗ [183]\n", "✗ [184]\n", "✗ [185]\n", "✗ [186]\n", "✗ [187]\n", "✗ [188]\n", "✗ [189]\n", "✗ [190]\n", "✗ [191]\n", "✗ [192]\n", "✗ [193]\n", "✗ [194]\n", "✗ [195]\n", "✗ [196]\n", "✗ [197]\n", "✗ [198]\n", "✗ [199]\n", "✗ [200]\n", "✗ [201]\n", "✗ [202]\n", "✗ [203]\n", "✗ [204]\n", "✗ [205]\n", "✗ [206]\n", "✗ [207]\n", "✗ [208]\n", "✗ [209]\n", "✗ [210]\n", "✗ [211]\n", "✗ [212]\n", "✗ [213]\n", "✗ [214]\n", "✗ [215]\n", "✗ [216]\n", "✗ [217]\n", "✗ [218]\n", "✗ [219]\n", "✗ [220]\n", "✗ [221]\n", "✗ [222]\n", "✗ [223]\n", "✗ [224]\n", "✗ [225]\n", "✗ [226]\n", "✗ [227]\n", "✗ [228]\n", "✗ [229]\n", "✗ [230]\n", "✗ [231]\n", "✗ [232]\n", "✗ [233]\n", "✗ [234]\n", "✗ [235]\n", "✗ [236]\n", "✗ [237]\n", "✗ [238]\n", "✗ [239]\n", "✗ [240]\n", "✗ [241]\n", "✗ [242]\n", "✗ [243]\n", "✗ [244]\n", "✗ [245]\n", "✗ [246]\n", "✗ [247]\n", "✗ [248]\n", "✗ [249]\n", "✗ [250]\n", "✗ [251]\n", "✗ [252]\n", "✗ [253]\n", "✗ [254]\n", "✗ [255]\n", "✗ [256]\n", "✗ [257]\n", "✗ [258]\n", "✗ [259]\n", "✗ [260]\n", "✗ [261]\n", "✗ [262]\n", "✗ [263]\n", "✗ [264]\n", "✗ [265]\n", "✗ [266]\n", "✗ [267]\n", "✗ [268]\n", "✗ [269]\n", "✗ [270]\n", "✗ [271]\n", "✗ [272]\n", "✗ [273]\n", "✗ [274]\n", "✗ [275]\n", "✗ [276]\n", "✗ [277]\n", "✗ [278]\n", "✗ [279]\n", "✗ [280]\n", "✗ [281]\n", "✗ [282]\n", "✗ [283]\n", "✗ [284]\n", "✗ [285]\n", "✗ [286]\n", "✗ [287]\n", "✗ [288]\n", "✗ [289]\n", "✗ [290]\n", "✗ [291]\n", "✗ [292]\n", "✗ [293]\n", "✗ [294]\n", "✗ [295]\n", "✗ [296]\n", "✗ [297]\n", "✗ [298]\n", "✗ [299]\n", "✗ [300]\n", "✗ [301]\n", "✗ [302]\n", "✗ [303]\n", "✗ [304]\n", "✗ [305]\n", "✗ [306]\n", "✗ [307]\n", "✗ [308]\n", "✗ [309]\n", "✗ [310]\n", "✗ [311]\n", "✗ [312]\n", "✗ [313]\n", "✗ [314]\n", "✗ [315]\n", "✗ [316]\n", "✗ [317]\n", "✗ [318]\n", "✗ [319]\n", "✗ [320]\n", "✗ [321]\n", "✗ [322]\n", "✗ [323]\n", "✗ [324]\n", "✗ [325]\n", "✗ [326]\n", "✗ [327]\n", "✗ [328]\n", "✗ [329]\n", "✗ [330]\n", "✗ [331]\n", "✗ [332]\n", "✗ [333]\n", "✗ [334]\n", "✗ [335]\n", "✗ [336]\n", "✗ [337]\n", "✗ [338]\n", "✗ [339]\n", "✗ [340]\n", "✗ [341]\n", "✗ [342]\n", "✗ [343]\n", "✗ [344]\n", "✗ [345]\n", "✗ [346]\n", "✗ [347]\n", "✗ [348]\n", "✗ [349]\n", "✗ [350]\n", "✗ [351]\n", "✗ [352]\n", "✗ [353]\n", "✗ [354]\n", "✗ [355]\n", "✗ [356]\n", "✗ [357]\n", "✗ [358]\n", "✗ [359]\n", "✗ [360]\n", "✗ [361]\n", "✗ [362]\n", "✗ [363]\n", "✗ [364]\n", "✗ [365]\n", "✗ [366]\n", "✗ [367]\n", "✗ [368]\n", "✗ [369]\n", "✗ [370]\n", "✗ [371]\n", "✗ [372]\n", "✗ [373]\n", "✗ [374]\n", "✗ [375]\n", "✗ [376]\n", "✗ [377]\n", "✗ [378]\n", "✗ [379]\n", "✗ [380]\n", "✗ [381]\n", "✗ [382]\n", "✗ [383]\n", "✗ [384]\n", "✗ [385]\n", "✗ [386]\n", "✗ [387]\n", "✗ [388]\n", "✗ [389]\n", "✗ [390]\n", "✗ [391]\n", "✗ [392]\n", "✗ [393]\n", "✗ [394]\n", "✗ [395]\n", "✗ [396]\n", "✗ [397]\n", "✗ [398]\n", "✗ [399]\n", "✗ [400]\n", "✗ [401]\n", "✗ [402]\n", "✗ [403]\n", "✗ [404]\n", "✗ [405]\n", "✗ [406]\n", "✗ [407]\n", "✗ [408]\n", "✗ [409]\n", "✗ [410]\n", "✗ [411]\n", "✗ [412]\n", "✗ [413]\n", "✗ [414]\n", "✗ [415]\n", "✗ [416]\n", "✗ [417]\n", "✗ [418]\n", "✗ [419]\n", "✗ [420]\n", "✗ [421]\n", "✗ [422]\n", "✗ [423]\n", "✗ [424]\n", "✗ [425]\n", "✗ [426]\n", "✗ [427]\n", "✗ [428]\n", "✗ [429]\n", "✗ [430]\n", "✗ [431]\n", "✗ [432]\n", "✗ [433]\n", "✗ [434]\n", "✗ [435]\n", "✗ [436]\n", "✗ [437]\n", "✗ [438]\n", "✗ [439]\n", "✗ [440]\n", "✗ [441]\n", "✗ [442]\n", "✗ [443]\n", "✗ [444]\n", "✗ [445]\n", "✗ [446]\n", "✗ [447]\n", "✗ [448]\n", "✗ [449]\n", "✗ [450]\n", "✗ [451]\n", "✗ [452]\n", "✗ [453]\n", "✗ [454]\n", "✗ [455]\n", "✗ [456]\n", "✗ [457]\n", "✗ [458]\n", "✗ [459]\n", "✗ [460]\n", "✗ [461]\n", "✗ [462]\n", "✗ [463]\n", "✗ [464]\n", "✗ [465]\n", "✗ [466]\n", "✗ [467]\n", "✗ [468]\n", "✗ [469]\n", "✗ [470]\n", "✗ [471]\n", "✗ [472]\n", "✗ [473]\n", "✗ [474]\n", "✗ [475]\n", "✗ [476]\n", "✗ [477]\n", "✗ [478]\n", "✗ [479]\n", "✗ [480]\n", "✗ [481]\n", "✗ [482]\n", "✗ [483]\n", "✗ [484]\n", "✗ [485]\n", "✗ [486]\n", "✗ [487]\n", "✗ [488]\n", "✗ [489]\n", "✗ [490]\n", "✗ [491]\n", "✗ [492]\n", "✗ [493]\n", "✗ [494]\n", "✗ [495]\n", "✗ [496]\n", "✗ [497]\n", "✗ [498]\n", "✗ [499]\n", "✓ [500]\n", "✗ []\n", "✗ [0]\n", "✗ [1]\n", "✗ [2]\n", "✗ [3]\n", "✗ [4]\n", "✗ [5]\n", "✗ [6]\n", "✗ [7]\n", "✗ [8]\n", "✗ [9]\n", "✗ [10]\n", "✗ [11]\n", "✗ [12]\n", "✗ [13]\n", "✗ [14]\n", "✗ [15]\n", "✗ [16]\n", "✗ [17]\n", "✗ [18]\n", "✗ [19]\n", "✗ [20]\n", "✗ [21]\n", "✗ [22]\n", "✗ [23]\n", "✗ [24]\n", "✗ [25]\n", "✗ [26]\n", "✗ [27]\n", "✗ [28]\n", "✗ [29]\n", "✗ [30]\n", "✗ [31]\n", "✗ [32]\n", "✗ [33]\n", "✗ [34]\n", "✗ [35]\n", "✗ [36]\n", "✗ [37]\n", "✗ [38]\n", "✗ [39]\n", "✗ [40]\n", "✗ [41]\n", "✗ [42]\n", "✗ [43]\n", "✗ [44]\n", "✗ [45]\n", "✗ [46]\n", "✗ [47]\n", "✗ [48]\n", "✗ [49]\n", "✗ [50]\n", "✗ [51]\n", "✗ [52]\n", "✗ [53]\n", "✗ [54]\n", "✗ [55]\n", "✗ [56]\n", "✗ [57]\n", "✗ [58]\n", "✗ [59]\n", "✗ [60]\n", "✗ [61]\n", "✗ [62]\n", "✗ [63]\n", "✗ [64]\n", "✗ [65]\n", "✗ [66]\n", "✗ [67]\n", "✗ [68]\n", "✗ [69]\n", "✗ [70]\n", "✗ [71]\n", "✗ [72]\n", "✗ [73]\n", "✗ [74]\n", "✗ [75]\n", "✗ [76]\n", "✗ [77]\n", "✗ [78]\n", "✗ [79]\n", "✗ [80]\n", "✗ [81]\n", "✗ [82]\n", "✗ [83]\n", "✗ [84]\n", "✗ [85]\n", "✗ [86]\n", "✗ [87]\n", "✗ [88]\n", "✗ [89]\n", "✗ [90]\n", "✗ [91]\n", "✗ [92]\n", "✗ [93]\n", "✗ [94]\n", "✗ [95]\n", "✗ [96]\n", "✗ [97]\n", "✗ [98]\n", "✗ [99]\n", "✗ [100]\n", "✗ [101]\n", "✗ [102]\n", "✗ [103]\n", "✗ [104]\n", "✗ [105]\n", "✗ [106]\n", "✗ [107]\n", "✗ [108]\n", "✗ [109]\n", "✗ [110]\n", "✗ [111]\n", "✗ [112]\n", "✗ [113]\n", "✗ [114]\n", "✗ [115]\n", "✗ [116]\n", "✗ [117]\n", "✗ [118]\n", "✗ [119]\n", "✗ [120]\n", "✗ [121]\n", "✗ [122]\n", "✗ [123]\n", "✗ [124]\n", "✗ [125]\n", "✗ [126]\n", "✗ [127]\n", "✗ [128]\n", "✗ [129]\n", "✗ [130]\n", "✗ [131]\n", "✗ [132]\n", "✗ [133]\n", "✗ [134]\n", "✗ [135]\n", "✗ [136]\n", "✗ [137]\n", "✗ [138]\n", "✗ [139]\n", "✗ [140]\n", "✗ [141]\n", "✗ [142]\n", "✗ [143]\n", "✗ [144]\n", "✗ [145]\n", "✗ [146]\n", "✗ [147]\n", "✗ [148]\n", "✗ [149]\n", "✗ [150]\n", "✗ [151]\n", "✗ [152]\n", "✗ [153]\n", "✗ [154]\n", "✗ [155]\n", "✗ [156]\n", "✗ [157]\n", "✗ [158]\n", "✗ [159]\n", "✗ [160]\n", "✗ [161]\n", "✗ [162]\n", "✗ [163]\n", "✗ [164]\n", "✗ [165]\n", "✗ [166]\n", "✗ [167]\n", "✗ [168]\n", "✗ [169]\n", "✗ [170]\n", "✗ [171]\n", "✗ [172]\n", "✗ [173]\n", "✗ [174]\n", "✗ [175]\n", "✗ [176]\n", "✗ [177]\n", "✗ [178]\n", "✗ [179]\n", "✗ [180]\n", "✗ [181]\n", "✗ [182]\n", "✗ [183]\n", "✗ [184]\n", "✗ [185]\n", "✗ [186]\n", "✗ [187]\n", "✗ [188]\n", "✗ [189]\n", "✗ [190]\n", "✗ [191]\n", "✗ [192]\n", "✗ [193]\n", "✗ [194]\n", "✗ [195]\n", "✗ [196]\n", "✗ [197]\n", "✗ [198]\n", "✗ [199]\n", "✗ [200]\n", "✗ [201]\n", "✗ [202]\n", "✗ [203]\n", "✗ [204]\n", "✗ [205]\n", "✗ [206]\n", "✗ [207]\n", "✗ [208]\n", "✗ [209]\n", "✗ [210]\n", "✗ [211]\n", "✗ [212]\n", "✗ [213]\n", "✗ [214]\n", "✗ [215]\n", "✗ [216]\n", "✗ [217]\n", "✗ [218]\n", "✗ [219]\n", "✗ [220]\n", "✗ [221]\n", "✗ [222]\n", "✗ [223]\n", "✗ [224]\n", "✗ [225]\n", "✗ [226]\n", "✗ [227]\n", "✗ [228]\n", "✗ [229]\n", "✗ [230]\n", "✗ [231]\n", "✗ [232]\n", "✗ [233]\n", "✗ [234]\n", "✗ [235]\n", "✗ [236]\n", "✗ [237]\n", "✗ [238]\n", "✗ [239]\n", "✗ [240]\n", "✗ [241]\n", "✗ [242]\n", "✗ [243]\n", "✗ [244]\n", "✗ [245]\n", "✗ [246]\n", "✗ [247]\n", "✗ [248]\n", "✗ [249]\n", "✗ [250]\n", "✗ [251]\n", "✗ [252]\n", "✗ [253]\n", "✗ [254]\n", "✗ [255]\n", "✗ [256]\n", "✗ [257]\n", "✗ [258]\n", "✗ [259]\n", "✗ [260]\n", "✗ [261]\n", "✗ [262]\n", "✗ [263]\n", "✗ [264]\n", "✗ [265]\n", "✗ [266]\n", "✗ [267]\n", "✗ [268]\n", "✗ [269]\n", "✗ [270]\n", "✗ [271]\n", "✗ [272]\n", "✗ [273]\n", "✗ [274]\n", "✗ [275]\n", "✗ [276]\n", "✗ [277]\n", "✗ [278]\n", "✗ [279]\n", "✗ [280]\n", "✗ [281]\n", "✗ [282]\n", "✗ [283]\n", "✗ [284]\n", "✗ [285]\n", "✗ [286]\n", "✗ [287]\n", "✗ [288]\n", "✗ [289]\n", "✗ [290]\n", "✗ [291]\n", "✗ [292]\n", "✗ [293]\n", "✗ [294]\n", "✗ [295]\n", "✗ [296]\n", "✗ [297]\n", "✗ [298]\n", "✗ [299]\n", "✗ [300]\n", "✗ [301]\n", "✗ [302]\n", "✗ [303]\n", "✗ [304]\n", "✗ [305]\n", "✗ [306]\n", "✗ [307]\n", "✗ [308]\n", "✗ [309]\n", "✗ [310]\n", "✗ [311]\n", "✗ [312]\n", "✗ [313]\n", "✗ [314]\n", "✗ [315]\n", "✗ [316]\n", "✗ [317]\n", "✗ [318]\n", "✗ [319]\n", "✗ [320]\n", "✗ [321]\n", "✗ [322]\n", "✗ [323]\n", "✗ [324]\n", "✗ [325]\n", "✗ [326]\n", "✗ [327]\n", "✗ [328]\n", "✗ [329]\n", "✗ [330]\n", "✗ [331]\n", "✗ [332]\n", "✗ [333]\n", "✗ [334]\n", "✗ [335]\n", "✗ [336]\n", "✗ [337]\n", "✗ [338]\n", "✗ [339]\n", "✗ [340]\n", "✗ [341]\n", "✗ [342]\n", "✗ [343]\n", "✗ [344]\n", "✗ [345]\n", "✗ [346]\n", "✗ [347]\n", "✗ [348]\n", "✗ [349]\n", "✗ [350]\n", "✗ [351]\n", "✗ [352]\n", "✗ [353]\n", "✗ [354]\n", "✗ [355]\n", "✗ [356]\n", "✗ [357]\n", "✗ [358]\n", "✗ [359]\n", "✗ [360]\n", "✗ [361]\n", "✗ [362]\n", "✗ [363]\n", "✗ [364]\n", "✗ [365]\n", "✗ [366]\n", "✗ [367]\n", "✗ [368]\n", "✗ [369]\n", "✗ [370]\n", "✗ [371]\n", "✗ [372]\n", "✗ [373]\n", "✗ [374]\n", "✗ [375]\n", "✗ [376]\n", "✗ [377]\n", "✗ [378]\n", "✗ [379]\n", "✗ [380]\n", "✗ [381]\n", "✗ [382]\n", "✗ [383]\n", "✗ [384]\n", "✗ [385]\n", "✗ [386]\n", "✗ [387]\n", "✗ [388]\n", "✗ [389]\n", "✗ [390]\n", "✗ [391]\n", "✗ [392]\n", "✗ [393]\n", "✗ [394]\n", "✗ [395]\n", "✗ [396]\n", "✗ [397]\n", "✗ [398]\n", "✗ [399]\n", "✗ [400]\n", "✗ [401]\n", "✗ [402]\n", "✗ [403]\n", "✗ [404]\n", "✗ [405]\n", "✗ [406]\n", "✗ [407]\n", "✗ [408]\n", "✗ [409]\n", "✗ [410]\n", "✗ [411]\n", "✗ [412]\n", "✗ [413]\n", "✗ [414]\n", "✗ [415]\n", "✗ [416]\n", "✗ [417]\n", "✗ [418]\n", "✗ [419]\n", "✗ [420]\n", "✗ [421]\n", "✗ [422]\n", "✗ [423]\n", "✗ [424]\n", "✗ [425]\n", "✗ [426]\n", "✗ [427]\n", "✗ [428]\n", "✗ [429]\n", "✗ [430]\n", "✗ [431]\n", "✗ [432]\n", "✗ [433]\n", "✗ [434]\n", "✗ [435]\n", "✗ [436]\n", "✗ [437]\n", "✗ [438]\n", "✗ [439]\n", "✗ [440]\n", "✗ [441]\n", "✗ [442]\n", "✗ [443]\n", "✗ [444]\n", "✗ [445]\n", "✗ [446]\n", "✗ [447]\n", "✗ [448]\n", "✗ [449]\n", "✗ [450]\n", "✗ [451]\n", "✗ [452]\n", "✗ [453]\n", "✗ [454]\n", "✗ [455]\n", "✗ [456]\n", "✗ [457]\n", "✗ [458]\n", "✗ [459]\n", "✗ [460]\n", "✗ [461]\n", "✗ [462]\n", "✗ [463]\n", "✗ [464]\n", "✗ [465]\n", "✗ [466]\n", "✗ [467]\n", "✗ [468]\n", "✗ [469]\n", "✗ [470]\n", "✗ [471]\n", "✗ [472]\n", "✗ [473]\n", "✗ [474]\n", "✗ [475]\n", "✗ [476]\n", "✗ [477]\n", "✗ [478]\n", "✗ [479]\n", "✗ [480]\n", "✗ [481]\n", "✗ [482]\n", "✗ [483]\n", "✗ [484]\n", "✗ [485]\n", "✗ [486]\n", "✗ [487]\n", "✗ [488]\n", "✗ [489]\n", "✗ [490]\n", "✗ [491]\n", "✗ [492]\n", "✗ [493]\n", "✗ [494]\n", "✗ [495]\n", "✗ [496]\n", "✗ [497]\n", "✗ [498]\n", "✗ [499]\n", "\n", "1 shrinks with 1003 function calls\n" ] } ], "source": [ "show_trace([1000], lambda x: sum(x) >= 500, partial(greedy_shrink, shrink=shrink2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Because we're trying every intermediate value, what we have amounts to a linear probe up to the smallest value that will work. If that smallest value is large, this will take a long time. Our shrinking is still O(n), but n is now the size of the smallest value that will work rather than the starting value. This is still pretty suboptimal.\n", "\n", "What we want to do is try to replace our linear probe with a binary search. What we'll get isn't exactly a binary search, but it's close enough." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def shrink_integer(n):\n", " \"\"\"\n", " Shrinker for individual integers.\n", "\n", " What happens is that we start from the left, first probing upwards in powers of two.\n", "\n", " When this would take us past our target value we then binary chop towards it.\n", " \"\"\"\n", " if not n:\n", " return\n", " for k in range(64):\n", " probe = 2**k\n", " if probe >= n:\n", " break\n", " yield probe - 1\n", " probe //= 2\n", " while True:\n", " probe = (probe + n) // 2\n", " yield probe\n", " if probe == n - 1:\n", " break\n", "\n", "\n", "def shrink3(ls):\n", " for i in range(len(ls)):\n", " s = list(ls)\n", " del s[i]\n", " yield list(s)\n", " for x in shrink_integer(ls[i]):\n", " s = list(ls)\n", " s[i] = x\n", " yield s" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "[0, 1, 3, 7, 15, 31, 63, 127, 255, 378, 439, 469, 484, 492, 496, 498, 499]" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(shrink_integer(500))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This gives us a reasonable distribution of O(log(n)) values in the middle while still making sure we start with 0 and finish with n - 1.\n", "\n", "In Hypothesis's actual implementation we also try random values in the probe region in case there's something special about things near powers of two, but we won't worry about that here." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [1000]\n", "✗ []\n", "✗ [0]\n", "✗ [1]\n", "✗ [3]\n", "✗ [7]\n", "✗ [15]\n", "✗ [31]\n", "✗ [63]\n", "✗ [127]\n", "✗ [255]\n", "✓ [511]\n", "✗ []\n", "✗ [0]\n", "✗ [1]\n", "✗ [3]\n", "✗ [7]\n", "✗ [15]\n", "✗ [31]\n", "✗ [63]\n", "✗ [127]\n", "✗ [255]\n", "✗ [383]\n", "✗ [447]\n", "✗ [479]\n", "✗ [495]\n", "✓ [503]\n", "✗ []\n", "✗ [0]\n", "✗ [1]\n", "✗ [3]\n", "✗ [7]\n", "✗ [15]\n", "✗ [31]\n", "✗ [63]\n", "✗ [127]\n", "✗ [255]\n", "✗ [379]\n", "✗ [441]\n", "✗ [472]\n", "✗ [487]\n", "✗ [495]\n", "✗ [499]\n", "✓ [501]\n", "✗ []\n", "✗ [0]\n", "✗ [1]\n", "✗ [3]\n", "✗ [7]\n", "✗ [15]\n", "✗ [31]\n", "✗ [63]\n", "✗ [127]\n", "✗ [255]\n", "✗ [378]\n", "✗ [439]\n", "✗ [470]\n", "✗ [485]\n", "✗ [493]\n", "✗ [497]\n", "✗ [499]\n", "✓ [500]\n", "✗ []\n", "✗ [0]\n", "✗ [1]\n", "✗ [3]\n", "✗ [7]\n", "✗ [15]\n", "✗ [31]\n", "✗ [63]\n", "✗ [127]\n", "✗ [255]\n", "✗ [378]\n", "✗ [439]\n", "✗ [469]\n", "✗ [484]\n", "✗ [492]\n", "✗ [496]\n", "✗ [498]\n", "✗ [499]\n", "\n", "4 shrinks with 79 function calls\n" ] } ], "source": [ "show_trace([1000], lambda x: sum(x) >= 500, partial(greedy_shrink, shrink=shrink3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This now runs in a much more reasonable number of function calls.\n", "\n", "Now we want to look at how to reduce the number of elements in the list more efficiently. We're currently making the same mistake we did with n umbers. Only reducing one at a time." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2, 2]\n", "✓ [2, 2, 2, 2]\n", "✓ [2, 2, 2]\n", "✓ [2, 2]\n", "✗ [2]\n", "✗ [0, 2]\n", "✓ [1, 2]\n", "✗ [2]\n", "✗ [0, 2]\n", "✗ [1]\n", "✗ [1, 0]\n", "✗ [1, 1]\n", "\n", "19 shrinks with 26 function calls\n" ] } ], "source": [ "show_trace([2] * 20, lambda x: sum(x) >= 3, partial(greedy_shrink, shrink=shrink3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We won't try too hard here, because typically our lists are not *that* long. We will just attempt to start by finding a shortish initial prefix that demonstrates the behaviour:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def shrink_to_prefix(ls):\n", " i = 1\n", " while i < len(ls):\n", " yield ls[:i]\n", " i *= 2\n", "\n", "\n", "def delete_individual_elements(ls):\n", " for i in range(len(ls)):\n", " s = list(ls)\n", " del s[i]\n", " yield list(s)\n", "\n", "\n", "def shrink_individual_elements(ls):\n", " for i in range(len(ls)):\n", " for x in shrink_integer(ls[i]):\n", " s = list(ls)\n", " s[i] = x\n", " yield s\n", "\n", "\n", "def shrink4(ls):\n", " yield from shrink_to_prefix(ls)\n", " yield from delete_individual_elements(ls)\n", " yield from shrink_individual_elements(ls)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [2]\n", "✓ [2, 2]\n", "✗ [2]\n", "✗ [2]\n", "✗ [2]\n", "✗ [0, 2]\n", "✓ [1, 2]\n", "✗ [1]\n", "✗ [2]\n", "✗ [1]\n", "✗ [0, 2]\n", "✗ [1, 0]\n", "✗ [1, 1]\n", "\n", "2 shrinks with 13 function calls\n" ] } ], "source": [ "show_trace([2] * 20, lambda x: sum(x) >= 3, partial(greedy_shrink, shrink=shrink4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The problem we now want to address is the fact that when we're shrinking elements we're only shrinking them one at a time. This means that even though we're only O(log(k)) in each element, we're O(log(k)^n) in the whole list where n is the length of the list. For even very modest k this is bad.\n", "\n", "In general we may not be able to fix this, but in practice for a lot of common structures we can exploit similarity to try to do simultaneous shrinking.\n", "\n", "Here is our starting example: We start and finish with all identical values. We would like to be able to shortcut through a lot of the uninteresting intermediate examples somehow." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [20, 20, 20, 20, 20, 20, 20]\n", "✗ [20]\n", "✗ [20, 20]\n", "✗ [20, 20, 20, 20]\n", "✓ [20, 20, 20, 20, 20, 20]\n", "✗ [20]\n", "✗ [20, 20]\n", "✗ [20, 20, 20, 20]\n", "✓ [20, 20, 20, 20, 20]\n", "✗ [20]\n", "✗ [20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [0, 20, 20, 20, 20]\n", "✗ [1, 20, 20, 20, 20]\n", "✗ [3, 20, 20, 20, 20]\n", "✓ [7, 20, 20, 20, 20]\n", "✗ [7]\n", "✗ [7, 20]\n", "✗ [7, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [7, 20, 20, 20]\n", "✗ [7, 20, 20, 20]\n", "✗ [7, 20, 20, 20]\n", "✗ [7, 20, 20, 20]\n", "✗ [0, 20, 20, 20, 20]\n", "✗ [1, 20, 20, 20, 20]\n", "✗ [3, 20, 20, 20, 20]\n", "✓ [5, 20, 20, 20, 20]\n", "✗ [5]\n", "✗ [5, 20]\n", "✗ [5, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [5, 20, 20, 20]\n", "✗ [5, 20, 20, 20]\n", "✗ [5, 20, 20, 20]\n", "✗ [5, 20, 20, 20]\n", "✗ [0, 20, 20, 20, 20]\n", "✗ [1, 20, 20, 20, 20]\n", "✗ [3, 20, 20, 20, 20]\n", "✗ [4, 20, 20, 20, 20]\n", "✗ [5, 0, 20, 20, 20]\n", "✗ [5, 1, 20, 20, 20]\n", "✗ [5, 3, 20, 20, 20]\n", "✓ [5, 7, 20, 20, 20]\n", "✗ [5]\n", "✗ [5, 7]\n", "✗ [5, 7, 20, 20]\n", "✗ [7, 20, 20, 20]\n", "✗ [5, 20, 20, 20]\n", "✗ [5, 7, 20, 20]\n", "✗ [5, 7, 20, 20]\n", "✗ [5, 7, 20, 20]\n", "✗ [0, 7, 20, 20, 20]\n", "✗ [1, 7, 20, 20, 20]\n", "✗ [3, 7, 20, 20, 20]\n", "✗ [4, 7, 20, 20, 20]\n", "✗ [5, 0, 20, 20, 20]\n", "✗ [5, 1, 20, 20, 20]\n", "✗ [5, 3, 20, 20, 20]\n", "✓ [5, 5, 20, 20, 20]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 20, 20]\n", "✗ [5, 20, 20, 20]\n", "✗ [5, 20, 20, 20]\n", "✗ [5, 5, 20, 20]\n", "✗ [5, 5, 20, 20]\n", "✗ [5, 5, 20, 20]\n", "✗ [0, 5, 20, 20, 20]\n", "✗ [1, 5, 20, 20, 20]\n", "✗ [3, 5, 20, 20, 20]\n", "✗ [4, 5, 20, 20, 20]\n", "✗ [5, 0, 20, 20, 20]\n", "✗ [5, 1, 20, 20, 20]\n", "✗ [5, 3, 20, 20, 20]\n", "✗ [5, 4, 20, 20, 20]\n", "✗ [5, 5, 0, 20, 20]\n", "✗ [5, 5, 1, 20, 20]\n", "✗ [5, 5, 3, 20, 20]\n", "✓ [5, 5, 7, 20, 20]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 7, 20]\n", "✗ [5, 7, 20, 20]\n", "✗ [5, 7, 20, 20]\n", "✗ [5, 5, 20, 20]\n", "✗ [5, 5, 7, 20]\n", "✗ [5, 5, 7, 20]\n", "✗ [0, 5, 7, 20, 20]\n", "✗ [1, 5, 7, 20, 20]\n", "✗ [3, 5, 7, 20, 20]\n", "✗ [4, 5, 7, 20, 20]\n", "✗ [5, 0, 7, 20, 20]\n", "✗ [5, 1, 7, 20, 20]\n", "✗ [5, 3, 7, 20, 20]\n", "✗ [5, 4, 7, 20, 20]\n", "✗ [5, 5, 0, 20, 20]\n", "✗ [5, 5, 1, 20, 20]\n", "✗ [5, 5, 3, 20, 20]\n", "✓ [5, 5, 5, 20, 20]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 20]\n", "✗ [5, 5, 20, 20]\n", "✗ [5, 5, 20, 20]\n", "✗ [5, 5, 20, 20]\n", "✗ [5, 5, 5, 20]\n", "✗ [5, 5, 5, 20]\n", "✗ [0, 5, 5, 20, 20]\n", "✗ [1, 5, 5, 20, 20]\n", "✗ [3, 5, 5, 20, 20]\n", "✗ [4, 5, 5, 20, 20]\n", "✗ [5, 0, 5, 20, 20]\n", "✗ [5, 1, 5, 20, 20]\n", "✗ [5, 3, 5, 20, 20]\n", "✗ [5, 4, 5, 20, 20]\n", "✗ [5, 5, 0, 20, 20]\n", "✗ [5, 5, 1, 20, 20]\n", "✗ [5, 5, 3, 20, 20]\n", "✗ [5, 5, 4, 20, 20]\n", "✗ [5, 5, 5, 0, 20]\n", "✗ [5, 5, 5, 1, 20]\n", "✗ [5, 5, 5, 3, 20]\n", "✓ [5, 5, 5, 7, 20]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 7, 20]\n", "✗ [5, 5, 7, 20]\n", "✗ [5, 5, 7, 20]\n", "✗ [5, 5, 5, 20]\n", "✗ [5, 5, 5, 7]\n", "✗ [0, 5, 5, 7, 20]\n", "✗ [1, 5, 5, 7, 20]\n", "✗ [3, 5, 5, 7, 20]\n", "✗ [4, 5, 5, 7, 20]\n", "✗ [5, 0, 5, 7, 20]\n", "✗ [5, 1, 5, 7, 20]\n", "✗ [5, 3, 5, 7, 20]\n", "✗ [5, 4, 5, 7, 20]\n", "✗ [5, 5, 0, 7, 20]\n", "✗ [5, 5, 1, 7, 20]\n", "✗ [5, 5, 3, 7, 20]\n", "✗ [5, 5, 4, 7, 20]\n", "✗ [5, 5, 5, 0, 20]\n", "✗ [5, 5, 5, 1, 20]\n", "✗ [5, 5, 5, 3, 20]\n", "✓ [5, 5, 5, 5, 20]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 20]\n", "✗ [5, 5, 5, 20]\n", "✗ [5, 5, 5, 20]\n", "✗ [5, 5, 5, 20]\n", "✗ [5, 5, 5, 5]\n", "✗ [0, 5, 5, 5, 20]\n", "✗ [1, 5, 5, 5, 20]\n", "✗ [3, 5, 5, 5, 20]\n", "✗ [4, 5, 5, 5, 20]\n", "✗ [5, 0, 5, 5, 20]\n", "✗ [5, 1, 5, 5, 20]\n", "✗ [5, 3, 5, 5, 20]\n", "✗ [5, 4, 5, 5, 20]\n", "✗ [5, 5, 0, 5, 20]\n", "✗ [5, 5, 1, 5, 20]\n", "✗ [5, 5, 3, 5, 20]\n", "✗ [5, 5, 4, 5, 20]\n", "✗ [5, 5, 5, 0, 20]\n", "✗ [5, 5, 5, 1, 20]\n", "✗ [5, 5, 5, 3, 20]\n", "✗ [5, 5, 5, 4, 20]\n", "✗ [5, 5, 5, 5, 0]\n", "✗ [5, 5, 5, 5, 1]\n", "✗ [5, 5, 5, 5, 3]\n", "✓ [5, 5, 5, 5, 7]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 5, 5]\n", "✗ [0, 5, 5, 5, 7]\n", "✗ [1, 5, 5, 5, 7]\n", "✗ [3, 5, 5, 5, 7]\n", "✗ [4, 5, 5, 5, 7]\n", "✗ [5, 0, 5, 5, 7]\n", "✗ [5, 1, 5, 5, 7]\n", "✗ [5, 3, 5, 5, 7]\n", "✗ [5, 4, 5, 5, 7]\n", "✗ [5, 5, 0, 5, 7]\n", "✗ [5, 5, 1, 5, 7]\n", "✗ [5, 5, 3, 5, 7]\n", "✗ [5, 5, 4, 5, 7]\n", "✗ [5, 5, 5, 0, 7]\n", "✗ [5, 5, 5, 1, 7]\n", "✗ [5, 5, 5, 3, 7]\n", "✗ [5, 5, 5, 4, 7]\n", "✗ [5, 5, 5, 5, 0]\n", "✗ [5, 5, 5, 5, 1]\n", "✗ [5, 5, 5, 5, 3]\n", "✓ [5, 5, 5, 5, 5]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [0, 5, 5, 5, 5]\n", "✗ [1, 5, 5, 5, 5]\n", "✗ [3, 5, 5, 5, 5]\n", "✗ [4, 5, 5, 5, 5]\n", "✗ [5, 0, 5, 5, 5]\n", "✗ [5, 1, 5, 5, 5]\n", "✗ [5, 3, 5, 5, 5]\n", "✗ [5, 4, 5, 5, 5]\n", "✗ [5, 5, 0, 5, 5]\n", "✗ [5, 5, 1, 5, 5]\n", "✗ [5, 5, 3, 5, 5]\n", "✗ [5, 5, 4, 5, 5]\n", "✗ [5, 5, 5, 0, 5]\n", "✗ [5, 5, 5, 1, 5]\n", "✗ [5, 5, 5, 3, 5]\n", "✗ [5, 5, 5, 4, 5]\n", "✗ [5, 5, 5, 5, 0]\n", "✗ [5, 5, 5, 5, 1]\n", "✗ [5, 5, 5, 5, 3]\n", "✗ [5, 5, 5, 5, 4]\n", "\n", "12 shrinks with 236 function calls\n" ] } ], "source": [ "show_trace(\n", " [20] * 7,\n", " lambda x: len([t for t in x if t >= 5]) >= 5,\n", " partial(greedy_shrink, shrink=shrink4),\n", ")" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def shrink_shared(ls):\n", " \"\"\"\n", " Look for all sets of shared indices and try to perform a simultaneous shrink on\n", " their value, replacing all of them at once.\n", "\n", " In actual Hypothesis we also try replacing only subsets of the values when there\n", " are more than two shared values, but we won't worry about that here.\n", " \"\"\"\n", " shared_indices = {}\n", " for i in range(len(ls)):\n", " shared_indices.setdefault(ls[i], []).append(i)\n", " for sharing in shared_indices.values():\n", " if len(sharing) > 1:\n", " for v in shrink_integer(ls[sharing[0]]):\n", " s = list(ls)\n", " for i in sharing:\n", " s[i] = v\n", " yield s\n", "\n", "\n", "def shrink5(ls):\n", " yield from shrink_to_prefix(ls)\n", " yield from delete_individual_elements(ls)\n", " yield from shrink_shared(ls)\n", " yield from shrink_individual_elements(ls)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [20, 20, 20, 20, 20, 20, 20]\n", "✗ [20]\n", "✗ [20, 20]\n", "✗ [20, 20, 20, 20]\n", "✓ [20, 20, 20, 20, 20, 20]\n", "✗ [20]\n", "✗ [20, 20]\n", "✗ [20, 20, 20, 20]\n", "✓ [20, 20, 20, 20, 20]\n", "✗ [20]\n", "✗ [20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [20, 20, 20, 20]\n", "✗ [0, 0, 0, 0, 0]\n", "✗ [1, 1, 1, 1, 1]\n", "✗ [3, 3, 3, 3, 3]\n", "✓ [7, 7, 7, 7, 7]\n", "✗ [7]\n", "✗ [7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [0, 0, 0, 0, 0]\n", "✗ [1, 1, 1, 1, 1]\n", "✗ [3, 3, 3, 3, 3]\n", "✓ [5, 5, 5, 5, 5]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [0, 0, 0, 0, 0]\n", "✗ [1, 1, 1, 1, 1]\n", "✗ [3, 3, 3, 3, 3]\n", "✗ [4, 4, 4, 4, 4]\n", "✗ [0, 5, 5, 5, 5]\n", "✗ [1, 5, 5, 5, 5]\n", "✗ [3, 5, 5, 5, 5]\n", "✗ [4, 5, 5, 5, 5]\n", "✗ [5, 0, 5, 5, 5]\n", "✗ [5, 1, 5, 5, 5]\n", "✗ [5, 3, 5, 5, 5]\n", "✗ [5, 4, 5, 5, 5]\n", "✗ [5, 5, 0, 5, 5]\n", "✗ [5, 5, 1, 5, 5]\n", "✗ [5, 5, 3, 5, 5]\n", "✗ [5, 5, 4, 5, 5]\n", "✗ [5, 5, 5, 0, 5]\n", "✗ [5, 5, 5, 1, 5]\n", "✗ [5, 5, 5, 3, 5]\n", "✗ [5, 5, 5, 4, 5]\n", "✗ [5, 5, 5, 5, 0]\n", "✗ [5, 5, 5, 5, 1]\n", "✗ [5, 5, 5, 5, 3]\n", "✗ [5, 5, 5, 5, 4]\n", "\n", "4 shrinks with 64 function calls\n" ] } ], "source": [ "show_trace(\n", " [20] * 7,\n", " lambda x: len([t for t in x if t >= 5]) >= 5,\n", " partial(greedy_shrink, shrink=shrink5),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This achieves the desired result. We rapidly progress through all of the intermediate stages. We do still have to perform individual shrinks at the end unfortunately (this is unavoidable), but the size of the elements is much smaller now so it takes less time.\n", "\n", "Unfortunately while this solves the problem in this case it's almost useless, because unless you find yourself in the exact right starting position it never does anything." ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [20, 21, 22, 23, 24, 25, 26]\n", "✗ [20]\n", "✗ [20, 21]\n", "✗ [20, 21, 22, 23]\n", "✓ [21, 22, 23, 24, 25, 26]\n", "✗ [21]\n", "✗ [21, 22]\n", "✗ [21, 22, 23, 24]\n", "✓ [22, 23, 24, 25, 26]\n", "✗ [22]\n", "✗ [22, 23]\n", "✗ [22, 23, 24, 25]\n", "✗ [23, 24, 25, 26]\n", "✗ [22, 24, 25, 26]\n", "✗ [22, 23, 25, 26]\n", "✗ [22, 23, 24, 26]\n", "✗ [22, 23, 24, 25]\n", "✗ [0, 23, 24, 25, 26]\n", "✗ [1, 23, 24, 25, 26]\n", "✗ [3, 23, 24, 25, 26]\n", "✓ [7, 23, 24, 25, 26]\n", "✗ [7]\n", "✗ [7, 23]\n", "✗ [7, 23, 24, 25]\n", "✗ [23, 24, 25, 26]\n", "✗ [7, 24, 25, 26]\n", "✗ [7, 23, 25, 26]\n", "✗ [7, 23, 24, 26]\n", "✗ [7, 23, 24, 25]\n", "✗ [0, 23, 24, 25, 26]\n", "✗ [1, 23, 24, 25, 26]\n", "✗ [3, 23, 24, 25, 26]\n", "✓ [5, 23, 24, 25, 26]\n", "✗ [5]\n", "✗ [5, 23]\n", "✗ [5, 23, 24, 25]\n", "✗ [23, 24, 25, 26]\n", "✗ [5, 24, 25, 26]\n", "✗ [5, 23, 25, 26]\n", "✗ [5, 23, 24, 26]\n", "✗ [5, 23, 24, 25]\n", "✗ [0, 23, 24, 25, 26]\n", "✗ [1, 23, 24, 25, 26]\n", "✗ [3, 23, 24, 25, 26]\n", "✗ [4, 23, 24, 25, 26]\n", "✗ [5, 0, 24, 25, 26]\n", "✗ [5, 1, 24, 25, 26]\n", "✗ [5, 3, 24, 25, 26]\n", "✓ [5, 7, 24, 25, 26]\n", "✗ [5]\n", "✗ [5, 7]\n", "✗ [5, 7, 24, 25]\n", "✗ [7, 24, 25, 26]\n", "✗ [5, 24, 25, 26]\n", "✗ [5, 7, 25, 26]\n", "✗ [5, 7, 24, 26]\n", "✗ [5, 7, 24, 25]\n", "✗ [0, 7, 24, 25, 26]\n", "✗ [1, 7, 24, 25, 26]\n", "✗ [3, 7, 24, 25, 26]\n", "✗ [4, 7, 24, 25, 26]\n", "✗ [5, 0, 24, 25, 26]\n", "✗ [5, 1, 24, 25, 26]\n", "✗ [5, 3, 24, 25, 26]\n", "✓ [5, 5, 24, 25, 26]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 24, 25]\n", "✗ [5, 24, 25, 26]\n", "✗ [5, 24, 25, 26]\n", "✗ [5, 5, 25, 26]\n", "✗ [5, 5, 24, 26]\n", "✗ [5, 5, 24, 25]\n", "✗ [0, 0, 24, 25, 26]\n", "✗ [1, 1, 24, 25, 26]\n", "✗ [3, 3, 24, 25, 26]\n", "✗ [4, 4, 24, 25, 26]\n", "✗ [0, 5, 24, 25, 26]\n", "✗ [1, 5, 24, 25, 26]\n", "✗ [3, 5, 24, 25, 26]\n", "✗ [4, 5, 24, 25, 26]\n", "✗ [5, 0, 24, 25, 26]\n", "✗ [5, 1, 24, 25, 26]\n", "✗ [5, 3, 24, 25, 26]\n", "✗ [5, 4, 24, 25, 26]\n", "✗ [5, 5, 0, 25, 26]\n", "✗ [5, 5, 1, 25, 26]\n", "✗ [5, 5, 3, 25, 26]\n", "✓ [5, 5, 7, 25, 26]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 7, 25]\n", "✗ [5, 7, 25, 26]\n", "✗ [5, 7, 25, 26]\n", "✗ [5, 5, 25, 26]\n", "✗ [5, 5, 7, 26]\n", "✗ [5, 5, 7, 25]\n", "✗ [0, 0, 7, 25, 26]\n", "✗ [1, 1, 7, 25, 26]\n", "✗ [3, 3, 7, 25, 26]\n", "✗ [4, 4, 7, 25, 26]\n", "✗ [0, 5, 7, 25, 26]\n", "✗ [1, 5, 7, 25, 26]\n", "✗ [3, 5, 7, 25, 26]\n", "✗ [4, 5, 7, 25, 26]\n", "✗ [5, 0, 7, 25, 26]\n", "✗ [5, 1, 7, 25, 26]\n", "✗ [5, 3, 7, 25, 26]\n", "✗ [5, 4, 7, 25, 26]\n", "✗ [5, 5, 0, 25, 26]\n", "✗ [5, 5, 1, 25, 26]\n", "✗ [5, 5, 3, 25, 26]\n", "✓ [5, 5, 5, 25, 26]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 25]\n", "✗ [5, 5, 25, 26]\n", "✗ [5, 5, 25, 26]\n", "✗ [5, 5, 25, 26]\n", "✗ [5, 5, 5, 26]\n", "✗ [5, 5, 5, 25]\n", "✗ [0, 0, 0, 25, 26]\n", "✗ [1, 1, 1, 25, 26]\n", "✗ [3, 3, 3, 25, 26]\n", "✗ [4, 4, 4, 25, 26]\n", "✗ [0, 5, 5, 25, 26]\n", "✗ [1, 5, 5, 25, 26]\n", "✗ [3, 5, 5, 25, 26]\n", "✗ [4, 5, 5, 25, 26]\n", "✗ [5, 0, 5, 25, 26]\n", "✗ [5, 1, 5, 25, 26]\n", "✗ [5, 3, 5, 25, 26]\n", "✗ [5, 4, 5, 25, 26]\n", "✗ [5, 5, 0, 25, 26]\n", "✗ [5, 5, 1, 25, 26]\n", "✗ [5, 5, 3, 25, 26]\n", "✗ [5, 5, 4, 25, 26]\n", "✗ [5, 5, 5, 0, 26]\n", "✗ [5, 5, 5, 1, 26]\n", "✗ [5, 5, 5, 3, 26]\n", "✓ [5, 5, 5, 7, 26]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 7, 26]\n", "✗ [5, 5, 7, 26]\n", "✗ [5, 5, 7, 26]\n", "✗ [5, 5, 5, 26]\n", "✗ [5, 5, 5, 7]\n", "✗ [0, 0, 0, 7, 26]\n", "✗ [1, 1, 1, 7, 26]\n", "✗ [3, 3, 3, 7, 26]\n", "✗ [4, 4, 4, 7, 26]\n", "✗ [0, 5, 5, 7, 26]\n", "✗ [1, 5, 5, 7, 26]\n", "✗ [3, 5, 5, 7, 26]\n", "✗ [4, 5, 5, 7, 26]\n", "✗ [5, 0, 5, 7, 26]\n", "✗ [5, 1, 5, 7, 26]\n", "✗ [5, 3, 5, 7, 26]\n", "✗ [5, 4, 5, 7, 26]\n", "✗ [5, 5, 0, 7, 26]\n", "✗ [5, 5, 1, 7, 26]\n", "✗ [5, 5, 3, 7, 26]\n", "✗ [5, 5, 4, 7, 26]\n", "✗ [5, 5, 5, 0, 26]\n", "✗ [5, 5, 5, 1, 26]\n", "✗ [5, 5, 5, 3, 26]\n", "✓ [5, 5, 5, 5, 26]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 26]\n", "✗ [5, 5, 5, 26]\n", "✗ [5, 5, 5, 26]\n", "✗ [5, 5, 5, 26]\n", "✗ [5, 5, 5, 5]\n", "✗ [0, 0, 0, 0, 26]\n", "✗ [1, 1, 1, 1, 26]\n", "✗ [3, 3, 3, 3, 26]\n", "✗ [4, 4, 4, 4, 26]\n", "✗ [0, 5, 5, 5, 26]\n", "✗ [1, 5, 5, 5, 26]\n", "✗ [3, 5, 5, 5, 26]\n", "✗ [4, 5, 5, 5, 26]\n", "✗ [5, 0, 5, 5, 26]\n", "✗ [5, 1, 5, 5, 26]\n", "✗ [5, 3, 5, 5, 26]\n", "✗ [5, 4, 5, 5, 26]\n", "✗ [5, 5, 0, 5, 26]\n", "✗ [5, 5, 1, 5, 26]\n", "✗ [5, 5, 3, 5, 26]\n", "✗ [5, 5, 4, 5, 26]\n", "✗ [5, 5, 5, 0, 26]\n", "✗ [5, 5, 5, 1, 26]\n", "✗ [5, 5, 5, 3, 26]\n", "✗ [5, 5, 5, 4, 26]\n", "✗ [5, 5, 5, 5, 0]\n", "✗ [5, 5, 5, 5, 1]\n", "✗ [5, 5, 5, 5, 3]\n", "✓ [5, 5, 5, 5, 7]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 5, 7]\n", "✗ [5, 5, 5, 5]\n", "✗ [0, 0, 0, 0, 7]\n", "✗ [1, 1, 1, 1, 7]\n", "✗ [3, 3, 3, 3, 7]\n", "✗ [4, 4, 4, 4, 7]\n", "✗ [0, 5, 5, 5, 7]\n", "✗ [1, 5, 5, 5, 7]\n", "✗ [3, 5, 5, 5, 7]\n", "✗ [4, 5, 5, 5, 7]\n", "✗ [5, 0, 5, 5, 7]\n", "✗ [5, 1, 5, 5, 7]\n", "✗ [5, 3, 5, 5, 7]\n", "✗ [5, 4, 5, 5, 7]\n", "✗ [5, 5, 0, 5, 7]\n", "✗ [5, 5, 1, 5, 7]\n", "✗ [5, 5, 3, 5, 7]\n", "✗ [5, 5, 4, 5, 7]\n", "✗ [5, 5, 5, 0, 7]\n", "✗ [5, 5, 5, 1, 7]\n", "✗ [5, 5, 5, 3, 7]\n", "✗ [5, 5, 5, 4, 7]\n", "✗ [5, 5, 5, 5, 0]\n", "✗ [5, 5, 5, 5, 1]\n", "✗ [5, 5, 5, 5, 3]\n", "✓ [5, 5, 5, 5, 5]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [0, 0, 0, 0, 0]\n", "✗ [1, 1, 1, 1, 1]\n", "✗ [3, 3, 3, 3, 3]\n", "✗ [4, 4, 4, 4, 4]\n", "✗ [0, 5, 5, 5, 5]\n", "✗ [1, 5, 5, 5, 5]\n", "✗ [3, 5, 5, 5, 5]\n", "✗ [4, 5, 5, 5, 5]\n", "✗ [5, 0, 5, 5, 5]\n", "✗ [5, 1, 5, 5, 5]\n", "✗ [5, 3, 5, 5, 5]\n", "✗ [5, 4, 5, 5, 5]\n", "✗ [5, 5, 0, 5, 5]\n", "✗ [5, 5, 1, 5, 5]\n", "✗ [5, 5, 3, 5, 5]\n", "✗ [5, 5, 4, 5, 5]\n", "✗ [5, 5, 5, 0, 5]\n", "✗ [5, 5, 5, 1, 5]\n", "✗ [5, 5, 5, 3, 5]\n", "✗ [5, 5, 5, 4, 5]\n", "✗ [5, 5, 5, 5, 0]\n", "✗ [5, 5, 5, 5, 1]\n", "✗ [5, 5, 5, 5, 3]\n", "✗ [5, 5, 5, 5, 4]\n", "\n", "12 shrinks with 264 function calls\n" ] } ], "source": [ "show_trace(\n", " [20 + i for i in range(7)],\n", " lambda x: len([t for t in x if t >= 5]) >= 5,\n", " partial(greedy_shrink, shrink=shrink5),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So what we're going to try to do is to try a simplification first which *creates* that exact right starting condition. Further it's one that will be potentially very useful even if we don't actually have the situation where we have shared shrinks.\n", "\n", "What we're going to do is we're going to use values from the list to act as evidence for how complex things need to be. Starting from the smallest, we'll try capping the array at each individual value and see what happens.\n", "\n", "As well as being potentially a very rapid shrink, this creates lists with lots of duplicates, which enables the simultaneous shrinking to shine." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def replace_with_simpler(ls):\n", " if not ls:\n", " return\n", " values = set(ls)\n", " values.remove(max(ls))\n", " values = sorted(values)\n", " for v in values:\n", " yield [min(v, l) for l in ls]\n", "\n", "\n", "def shrink6(ls):\n", " yield from shrink_to_prefix(ls)\n", " yield from delete_individual_elements(ls)\n", " yield from replace_with_simpler(ls)\n", " yield from shrink_shared(ls)\n", " yield from shrink_individual_elements(ls)" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [20, 21, 22, 23, 24, 25, 26]\n", "✗ [20]\n", "✗ [20, 21]\n", "✗ [20, 21, 22, 23]\n", "✓ [21, 22, 23, 24, 25, 26]\n", "✗ [21]\n", "✗ [21, 22]\n", "✗ [21, 22, 23, 24]\n", "✓ [22, 23, 24, 25, 26]\n", "✗ [22]\n", "✗ [22, 23]\n", "✗ [22, 23, 24, 25]\n", "✗ [23, 24, 25, 26]\n", "✗ [22, 24, 25, 26]\n", "✗ [22, 23, 25, 26]\n", "✗ [22, 23, 24, 26]\n", "✗ [22, 23, 24, 25]\n", "✓ [22, 22, 22, 22, 22]\n", "✗ [22]\n", "✗ [22, 22]\n", "✗ [22, 22, 22, 22]\n", "✗ [22, 22, 22, 22]\n", "✗ [22, 22, 22, 22]\n", "✗ [22, 22, 22, 22]\n", "✗ [22, 22, 22, 22]\n", "✗ [22, 22, 22, 22]\n", "✗ [0, 0, 0, 0, 0]\n", "✗ [1, 1, 1, 1, 1]\n", "✗ [3, 3, 3, 3, 3]\n", "✓ [7, 7, 7, 7, 7]\n", "✗ [7]\n", "✗ [7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [7, 7, 7, 7]\n", "✗ [0, 0, 0, 0, 0]\n", "✗ [1, 1, 1, 1, 1]\n", "✗ [3, 3, 3, 3, 3]\n", "✓ [5, 5, 5, 5, 5]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [5, 5, 5, 5]\n", "✗ [0, 0, 0, 0, 0]\n", "✗ [1, 1, 1, 1, 1]\n", "✗ [3, 3, 3, 3, 3]\n", "✗ [4, 4, 4, 4, 4]\n", "✗ [0, 5, 5, 5, 5]\n", "✗ [1, 5, 5, 5, 5]\n", "✗ [3, 5, 5, 5, 5]\n", "✗ [4, 5, 5, 5, 5]\n", "✗ [5, 0, 5, 5, 5]\n", "✗ [5, 1, 5, 5, 5]\n", "✗ [5, 3, 5, 5, 5]\n", "✗ [5, 4, 5, 5, 5]\n", "✗ [5, 5, 0, 5, 5]\n", "✗ [5, 5, 1, 5, 5]\n", "✗ [5, 5, 3, 5, 5]\n", "✗ [5, 5, 4, 5, 5]\n", "✗ [5, 5, 5, 0, 5]\n", "✗ [5, 5, 5, 1, 5]\n", "✗ [5, 5, 5, 3, 5]\n", "✗ [5, 5, 5, 4, 5]\n", "✗ [5, 5, 5, 5, 0]\n", "✗ [5, 5, 5, 5, 1]\n", "✗ [5, 5, 5, 5, 3]\n", "✗ [5, 5, 5, 5, 4]\n", "\n", "5 shrinks with 73 function calls\n" ] } ], "source": [ "show_trace(\n", " [20 + i for i in range(7)],\n", " lambda x: len([t for t in x if t >= 5]) >= 5,\n", " partial(greedy_shrink, shrink=shrink6),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we're going to start looking at some numbers.\n", "\n", "What we'll do is we'll generate 1000 random lists satisfying some predicate, and then simplify them down to the smallest possible examples satisfying those predicates. This lets us verify that these aren't just cherry-picked examples and our methods help in the general case. We fix the set of examples per predicate so that we're comparing like for like.\n", "\n", "A more proper statistical treatment would probably be a good idea." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from collections import OrderedDict\n", "\n", "conditions = OrderedDict(\n", " [\n", " (\"length >= 2\", lambda xs: len(xs) >= 2),\n", " (\"sum >= 500\", lambda xs: sum(xs) >= 500),\n", " (\"sum >= 3\", lambda xs: sum(xs) >= 3),\n", " (\"At least 10 by 5\", lambda xs: len([t for t in xs if t >= 5]) >= 10),\n", " ]\n", ")" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "[17861213645196285187,\n", " 15609796832515195084,\n", " 8808697621832673046,\n", " 1013319847337885109,\n", " 1252281976438780211,\n", " 15526909770962854196,\n", " 2065337703776048239,\n", " 11654092230944134701,\n", " 5554896851708700201,\n", " 17485190250805381572,\n", " 7700396730246958474,\n", " 402840882133605445,\n", " 5303116940477413125,\n", " 7459257850255946545,\n", " 10349184495871650178,\n", " 4361155591615075311,\n", " 15194020468024244632,\n", " 14428821588688846242,\n", " 5754975712549869618,\n", " 13740966788951413307,\n", " 15209704957418077856,\n", " 12562588328524673262,\n", " 8415556016795311987,\n", " 3993098291779210741,\n", " 16874756914619597640,\n", " 7932421182532982309,\n", " 1080869529149674704,\n", " 13878842261614060122,\n", " 229976195287031921,\n", " 8378461140013520338,\n", " 6189522326946191255,\n", " 16684625600934047114,\n", " 12533448641134015292,\n", " 10459192142175991903,\n", " 15688511015570391481,\n", " 3091340728247101611,\n", " 4034760776171697910,\n", " 6258572097778886531,\n", " 13555449085571665140,\n", " 6727488149749641424,\n", " 7125107819562430884,\n", " 1557872425804423698,\n", " 4810250441100696888,\n", " 10500486959813930693,\n", " 841300069403644975,\n", " 9278626999406014662,\n", " 17219731431761688449,\n", " 15650446646901259126,\n", " 8683172055034528265,\n", " 5138373693056086816,\n", " 4055877702343936882,\n", " 5696765901584750542,\n", " 7133363948804979946,\n", " 988518370429658551,\n", " 16302597472193523184,\n", " 579078764159525857,\n", " 10678347012503400890,\n", " 8433836779160269996,\n", " 13884258181758870664,\n", " 13594877609651310055]" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import random\n", "\n", "N_EXAMPLES = 1000\n", "\n", "datasets = {}\n", "\n", "\n", "def gen_list(rnd):\n", " return [random.getrandbits(64) for _ in range(random.randint(0, 100))]\n", "\n", "\n", "def dataset_for(condition):\n", " if condition in datasets:\n", " return datasets[condition]\n", " constraint = conditions[condition]\n", " dataset = []\n", " rnd = random.Random(condition)\n", " while len(dataset) < N_EXAMPLES:\n", " ls = gen_list(rnd)\n", " if constraint(ls):\n", " dataset.append(ls)\n", " datasets[condition] = dataset\n", " return dataset\n", "\n", "\n", "dataset_for(\"sum >= 3\")[1]" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "13" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# In order to avoid run-away cases where things will take basically forever\n", "# we cap at 5000 as \"you've taken too long. Stop it\". Because we're only ever\n", "# showing the worst case scenario we'll just display this as > 5000 if we ever\n", "# hit it and it won't distort statistics.\n", "MAX_COUNT = 5000\n", "\n", "\n", "class MaximumCountExceeded(Exception):\n", " pass\n", "\n", "\n", "def call_counts(condition, simplifier):\n", " constraint = conditions[condition]\n", " dataset = dataset_for(condition)\n", " counts = []\n", "\n", " for ex in dataset:\n", " counter = 0\n", "\n", " def run_and_count(ls):\n", " nonlocal counter\n", " counter += 1\n", " if counter > MAX_COUNT:\n", " raise MaximumCountExceeded\n", " return constraint(ls)\n", "\n", " try:\n", " simplifier(ex, run_and_count)\n", " counts.append(counter)\n", " except MaximumCountExceeded:\n", " counts.append(MAX_COUNT + 1)\n", " break\n", " return counts\n", "\n", "\n", "def worst_case(condition, simplifier):\n", " return max(call_counts(condition, simplifier))\n", "\n", "\n", "worst_case(\"length >= 2\", partial(greedy_shrink, shrink=shrink6))" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from IPython.display import HTML\n", "\n", "\n", "def compare_simplifiers(named_simplifiers):\n", " \"\"\"\n", " Given a list of (name, simplifier) pairs, output a table comparing\n", " the worst case performance of each on our current set of examples.\n", " \"\"\"\n", " html_fragments = []\n", " html_fragments.append(\"\\n\\n\")\n", " header = [\"Condition\"]\n", " header.extend(name for name, _ in named_simplifiers)\n", " for h in header:\n", " html_fragments.append(\"\" % (h,))\n", " html_fragments.append(\"\\n\\n\")\n", "\n", " for name in conditions:\n", " bits = [name.replace(\">\", \">\")]\n", " for _, simplifier in named_simplifiers:\n", " value = worst_case(name, simplifier)\n", " if value <= MAX_COUNT:\n", " bits.append(str(value))\n", " else:\n", " bits.append(\" > %d\" % (MAX_COUNT,))\n", " html_fragments.append(\" \")\n", " html_fragments.append(\" \".join(\"\" % (b,) for b in bits))\n", " html_fragments.append(\"\")\n", " html_fragments.append(\"\\n
%s
%s
\")\n", " return HTML(\"\\n\".join(html_fragments))" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", "\n", "
Condition23456
length >= 2 106 105 13 13 13
sum >= 500 1102 178 80 80 80
sum >= 3 108 107 9 9 9
At least 10 by 5 535 690 809 877 144
" ], "text/plain": [ "" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "compare_simplifiers(\n", " [\n", " (f.__name__[-1], partial(greedy_shrink, shrink=f))\n", " for f in [shrink2, shrink3, shrink4, shrink5, shrink6]\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So you can see from the above table, the iterations 2 through 5 were a little ambiguous ion that they helped a lot in the cases they were designed to help with but hurt in other cases. 6 however is clearly the best of the lot, being no worse than any of the others on any of the cases and often significantly better.\n", "\n", "Rather than continuing to refine our shrink further, we instead look to improvements to how we use shrinking. We'll start by noting a simple optimization: If you look at our traces above, we often checked the same example twice. We're only interested in deterministic conditions, so this isn't useful to do. So we'll start by simply pruning out all duplicates. This should have exactly the same set and order of successful shrinks but will avoid a bunch of redundant work." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def greedy_shrink_with_dedupe(ls, constraint, shrink):\n", " seen = set()\n", " while True:\n", " for s in shrink(ls):\n", " key = tuple(s)\n", " if key in seen:\n", " continue\n", " seen.add(key)\n", " if constraint(s):\n", " ls = s\n", " break\n", " else:\n", " return ls" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", "\n", "
ConditionNormalDeduped
length >= 2 13 6
sum >= 500 80 35
sum >= 3 9 6
At least 10 by 5 144 107
" ], "text/plain": [ "" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "compare_simplifiers(\n", " [\n", " (\"Normal\", partial(greedy_shrink, shrink=shrink6)),\n", " (\"Deduped\", partial(greedy_shrink_with_dedupe, shrink=shrink6)),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As expected, this is a significant improvement in some cases. It is logically impossible that it could ever make things worse, but it's nice that it makes it better.\n", "\n", "So far we've only looked at things where the interaction between elements was fairly light - the sum cases the values of other elements mattered a bit, but shrinking an integer could never enable other shrinks. Let's look at one where this is not the case: Where our condition is that we have at least 10 distinct elements." ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100]\n", "✗ [100, 101]\n", "✗ [100, 101, 102, 103]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107]\n", "✗ [101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100, 101, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100, 101, 102, 104, 105, 106, 107, 108, 109]\n", "✗ [100, 101, 102, 103, 105, 106, 107, 108, 109]\n", "✗ [100, 101, 102, 103, 104, 106, 107, 108, 109]\n", "✗ [100, 101, 102, 103, 104, 105, 107, 108, 109]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 108, 109]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 109]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 108]\n", "✗ [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]\n", "✗ [100, 101, 101, 101, 101, 101, 101, 101, 101, 101]\n", "✗ [100, 101, 102, 102, 102, 102, 102, 102, 102, 102]\n", "✗ [100, 101, 102, 103, 103, 103, 103, 103, 103, 103]\n", "✗ [100, 101, 102, 103, 104, 104, 104, 104, 104, 104]\n", "✗ [100, 101, 102, 103, 104, 105, 105, 105, 105, 105]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 106, 106, 106]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 107, 107]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 108, 108]\n", "✓ [0, 101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 101]\n", "✗ [0, 101, 102, 103]\n", "✗ [0, 101, 102, 103, 104, 105, 106, 107]\n", "✗ [101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 101, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 101, 102, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 101, 102, 103, 105, 106, 107, 108, 109]\n", "✗ [0, 101, 102, 103, 104, 106, 107, 108, 109]\n", "✗ [0, 101, 102, 103, 104, 105, 107, 108, 109]\n", "✗ [0, 101, 102, 103, 104, 105, 106, 108, 109]\n", "✗ [0, 101, 102, 103, 104, 105, 106, 107, 109]\n", "✗ [0, 101, 102, 103, 104, 105, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 101, 101, 101, 101, 101, 101, 101, 101, 101]\n", "✗ [0, 101, 102, 102, 102, 102, 102, 102, 102, 102]\n", "✗ [0, 101, 102, 103, 103, 103, 103, 103, 103, 103]\n", "✗ [0, 101, 102, 103, 104, 104, 104, 104, 104, 104]\n", "✗ [0, 101, 102, 103, 104, 105, 105, 105, 105, 105]\n", "✗ [0, 101, 102, 103, 104, 105, 106, 106, 106, 106]\n", "✗ [0, 101, 102, 103, 104, 105, 106, 107, 107, 107]\n", "✗ [0, 101, 102, 103, 104, 105, 106, 107, 108, 108]\n", "✗ [0, 0, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 102, 103]\n", "✗ [0, 1, 102, 103, 104, 105, 106, 107]\n", "✗ [1, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 102, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 102, 103, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 102, 103, 104, 106, 107, 108, 109]\n", "✗ [0, 1, 102, 103, 104, 105, 107, 108, 109]\n", "✗ [0, 1, 102, 103, 104, 105, 106, 108, 109]\n", "✗ [0, 1, 102, 103, 104, 105, 106, 107, 109]\n", "✗ [0, 1, 102, 103, 104, 105, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 102, 102, 102, 102, 102, 102, 102, 102]\n", "✗ [0, 1, 102, 103, 103, 103, 103, 103, 103, 103]\n", "✗ [0, 1, 102, 103, 104, 104, 104, 104, 104, 104]\n", "✗ [0, 1, 102, 103, 104, 105, 105, 105, 105, 105]\n", "✗ [0, 1, 102, 103, 104, 105, 106, 106, 106, 106]\n", "✗ [0, 1, 102, 103, 104, 105, 106, 107, 107, 107]\n", "✗ [0, 1, 102, 103, 104, 105, 106, 107, 108, 108]\n", "✗ [0, 0, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 3, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 3, 103]\n", "✗ [0, 1, 3, 103, 104, 105, 106, 107]\n", "✗ [1, 3, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 3, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 103, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 103, 104, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 103, 104, 105, 107, 108, 109]\n", "✗ [0, 1, 3, 103, 104, 105, 106, 108, 109]\n", "✗ [0, 1, 3, 103, 104, 105, 106, 107, 109]\n", "✗ [0, 1, 3, 103, 104, 105, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 3, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 3, 103, 103, 103, 103, 103, 103, 103]\n", "✗ [0, 1, 3, 103, 104, 104, 104, 104, 104, 104]\n", "✗ [0, 1, 3, 103, 104, 105, 105, 105, 105, 105]\n", "✗ [0, 1, 3, 103, 104, 105, 106, 106, 106, 106]\n", "✗ [0, 1, 3, 103, 104, 105, 106, 107, 107, 107]\n", "✗ [0, 1, 3, 103, 104, 105, 106, 107, 108, 108]\n", "✗ [0, 0, 3, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 103]\n", "✗ [0, 1, 2, 103, 104, 105, 106, 107]\n", "✗ [1, 2, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 2, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 103, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 103, 104, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 103, 104, 105, 107, 108, 109]\n", "✗ [0, 1, 2, 103, 104, 105, 106, 108, 109]\n", "✗ [0, 1, 2, 103, 104, 105, 106, 107, 109]\n", "✗ [0, 1, 2, 103, 104, 105, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 103, 103, 103, 103, 103, 103, 103]\n", "✗ [0, 1, 2, 103, 104, 104, 104, 104, 104, 104]\n", "✗ [0, 1, 2, 103, 104, 105, 105, 105, 105, 105]\n", "✗ [0, 1, 2, 103, 104, 105, 106, 106, 106, 106]\n", "✗ [0, 1, 2, 103, 104, 105, 106, 107, 107, 107]\n", "✗ [0, 1, 2, 103, 104, 105, 106, 107, 108, 108]\n", "✗ [0, 0, 2, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 104, 105, 106, 107]\n", "✗ [1, 2, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 2, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 104, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 104, 105, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 104, 105, 106, 108, 109]\n", "✗ [0, 1, 2, 3, 104, 105, 106, 107, 109]\n", "✗ [0, 1, 2, 3, 104, 105, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 104, 104, 104, 104, 104, 104]\n", "✗ [0, 1, 2, 3, 104, 105, 105, 105, 105, 105]\n", "✗ [0, 1, 2, 3, 104, 105, 106, 106, 106, 106]\n", "✗ [0, 1, 2, 3, 104, 105, 106, 107, 107, 107]\n", "✗ [0, 1, 2, 3, 104, 105, 106, 107, 108, 108]\n", "✗ [0, 0, 2, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 7, 105, 106, 107]\n", "✗ [1, 2, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 2, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 7, 105, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 7, 105, 106, 108, 109]\n", "✗ [0, 1, 2, 3, 7, 105, 106, 107, 109]\n", "✗ [0, 1, 2, 3, 7, 105, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 7, 7, 7, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 7, 105, 105, 105, 105, 105]\n", "✗ [0, 1, 2, 3, 7, 105, 106, 106, 106, 106]\n", "✗ [0, 1, 2, 3, 7, 105, 106, 107, 107, 107]\n", "✗ [0, 1, 2, 3, 7, 105, 106, 107, 108, 108]\n", "✗ [0, 0, 2, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 5, 105, 106, 107]\n", "✗ [1, 2, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 2, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 5, 105, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 5, 105, 106, 108, 109]\n", "✗ [0, 1, 2, 3, 5, 105, 106, 107, 109]\n", "✗ [0, 1, 2, 3, 5, 105, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 5, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 5, 105, 105, 105, 105, 105]\n", "✗ [0, 1, 2, 3, 5, 105, 106, 106, 106, 106]\n", "✗ [0, 1, 2, 3, 5, 105, 106, 107, 107, 107]\n", "✗ [0, 1, 2, 3, 5, 105, 106, 107, 108, 108]\n", "✗ [0, 0, 2, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 105, 106, 107]\n", "✗ [1, 2, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 2, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 105, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 105, 106, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 105, 106, 107, 109]\n", "✗ [0, 1, 2, 3, 4, 105, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 105, 105, 105, 105, 105]\n", "✗ [0, 1, 2, 3, 4, 105, 106, 106, 106, 106]\n", "✗ [0, 1, 2, 3, 4, 105, 106, 107, 107, 107]\n", "✗ [0, 1, 2, 3, 4, 105, 106, 107, 108, 108]\n", "✗ [0, 0, 2, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 7, 106, 107]\n", "✗ [1, 2, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 2, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 7, 106, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 7, 106, 107, 109]\n", "✗ [0, 1, 2, 3, 4, 7, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 7, 7, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 7, 106, 106, 106, 106]\n", "✗ [0, 1, 2, 3, 4, 7, 106, 107, 107, 107]\n", "✗ [0, 1, 2, 3, 4, 7, 106, 107, 108, 108]\n", "✗ [0, 0, 2, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 106, 107]\n", "✗ [1, 2, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 2, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 106, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 106, 107, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 106, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 106, 106, 106, 106]\n", "✗ [0, 1, 2, 3, 4, 5, 106, 107, 107, 107]\n", "✗ [0, 1, 2, 3, 4, 5, 106, 107, 108, 108]\n", "✗ [0, 0, 2, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 107]\n", "✗ [1, 2, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 2, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 107, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 107, 107, 107]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 107, 108, 108]\n", "✗ [0, 0, 2, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 107]\n", "✗ [1, 2, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 2, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 107, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 107, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 107, 107, 107]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 107, 108, 108]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7]\n", "✗ [1, 2, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 2, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 108]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 108, 108]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7]\n", "✗ [1, 2, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 2, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 15]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 15, 15]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7]\n", "✗ [1, 2, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 2, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 11]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 11, 11]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7]\n", "✗ [1, 2, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 2, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 9]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 9, 9]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7]\n", "✗ [1, 2, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 2, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 8]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 1]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 7]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7]\n", "✗ [1, 2, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 2, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 8]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 1]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 7]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7]\n", "✗ [1, 2, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 2, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 8]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 1]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 7]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0]\n", "✗ [0, 1]\n", "✗ [0, 1, 2, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7]\n", "✗ [1, 2, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 2, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8]\n", "✗ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", "✗ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]\n", "✗ [0, 1, 2, 2, 2, 2, 2, 2, 2, 2]\n", "✗ [0, 1, 2, 3, 3, 3, 3, 3, 3, 3]\n", "✗ [0, 1, 2, 3, 4, 4, 4, 4, 4, 4]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 6, 6]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 8]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 1]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 7]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 8]\n", "\n", "20 shrinks with 848 function calls\n" ] } ], "source": [ "show_trace(\n", " [100 + i for i in range(10)],\n", " lambda x: len(set(x)) >= 10,\n", " partial(greedy_shrink, shrink=shrink6),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This does not do very well at all.\n", "\n", "The reason it doesn't is that we keep trying useless shrinks. e.g. none of the shrinks done by shrink\\_to\\_prefix, replace\\_with\\_simpler or shrink\\_shared will ever do anything useful here.\n", "\n", "So let's switch to an approach where we try shrink types until they stop working and then we move on to the next type:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def multicourse_shrink1(ls, constraint):\n", " seen = set()\n", " for shrink in [\n", " shrink_to_prefix,\n", " replace_with_simpler,\n", " shrink_shared,\n", " shrink_individual_elements,\n", " ]:\n", " while True:\n", " for s in shrink(ls):\n", " key = tuple(s)\n", " if key in seen:\n", " continue\n", " seen.add(key)\n", " if constraint(s):\n", " ls = s\n", " break\n", " else:\n", " break\n", " return ls" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100]\n", "✗ [100, 101]\n", "✗ [100, 101, 102, 103]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107]\n", "✗ [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]\n", "✗ [100, 101, 101, 101, 101, 101, 101, 101, 101, 101]\n", "✗ [100, 101, 102, 102, 102, 102, 102, 102, 102, 102]\n", "✗ [100, 101, 102, 103, 103, 103, 103, 103, 103, 103]\n", "✗ [100, 101, 102, 103, 104, 104, 104, 104, 104, 104]\n", "✗ [100, 101, 102, 103, 104, 105, 105, 105, 105, 105]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 106, 106, 106]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 107, 107]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 108, 108]\n", "✓ [0, 101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 0, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 3, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 0, 3, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 0, 2, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 0, 2, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 0, 2, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 7, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 7, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 0, 2, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 5, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 5, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 0, 2, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 0, 2, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 7, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 7, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 0, 2, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 0, 2, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 15, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 15, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 11, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 11, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 9, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 9, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 1]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 7]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 8, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 15]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 15]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 8, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 11]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 11]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 0, 2, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 0, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 1, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 0, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 1, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 2, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 0, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 1, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 3, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 0, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 1, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 3, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 4, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 8]\n", "\n", "20 shrinks with 318 function calls\n" ] } ], "source": [ "show_trace(\n", " [100 + i for i in range(10)], lambda x: len(set(x)) >= 10, multicourse_shrink1\n", ")" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "collapsed": false }, "outputs": [], "source": [ "conditions[\"10 distinct elements\"] = lambda xs: len(set(xs)) >= 10" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", "\n", "
ConditionSingle passMulti pass
length >= 2 6 4
sum >= 500 35 34
sum >= 3 6 5
At least 10 by 5 107 58
10 distinct elements 623 320
" ], "text/plain": [ "" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "compare_simplifiers(\n", " [\n", " (\"Single pass\", partial(greedy_shrink_with_dedupe, shrink=shrink6)),\n", " (\"Multi pass\", multicourse_shrink1),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So that helped, but not as much as we'd have liked. It's saved us about half the calls, when really we wanted to save 90% of the calls.\n", "\n", "We're on the right track though. The problem is not that our solution isn't good, it's that it didn't go far enough: We're *still* making an awful lot of useless calls. The problem is that each time we shrink the element at index i we try shrinking the elements at indexes 0 through i - 1, and this will never work. So what we want to do is to break shrinking elements into a separate shrinker for each index:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def simplify_index(i):\n", " def accept(ls):\n", " if i >= len(ls):\n", " return\n", " for v in shrink_integer(ls[i]):\n", " s = list(ls)\n", " s[i] = v\n", " yield s\n", "\n", " return accept\n", "\n", "\n", "def shrinkers_for(ls):\n", " yield shrink_to_prefix\n", " yield delete_individual_elements\n", " yield replace_with_simpler\n", " yield shrink_shared\n", " for i in range(len(ls)):\n", " yield simplify_index(i)\n", "\n", "\n", "def multicourse_shrink2(ls, constraint):\n", " seen = set()\n", " for shrink in shrinkers_for(ls):\n", " while True:\n", " for s in shrink(ls):\n", " key = tuple(s)\n", " if key in seen:\n", " continue\n", " seen.add(key)\n", " if constraint(s):\n", " ls = s\n", " break\n", " else:\n", " break\n", " return ls" ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100]\n", "✗ [100, 101]\n", "✗ [100, 101, 102, 103]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107]\n", "✗ [101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100, 101, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [100, 101, 102, 104, 105, 106, 107, 108, 109]\n", "✗ [100, 101, 102, 103, 105, 106, 107, 108, 109]\n", "✗ [100, 101, 102, 103, 104, 106, 107, 108, 109]\n", "✗ [100, 101, 102, 103, 104, 105, 107, 108, 109]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 108, 109]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 109]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 108]\n", "✗ [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]\n", "✗ [100, 101, 101, 101, 101, 101, 101, 101, 101, 101]\n", "✗ [100, 101, 102, 102, 102, 102, 102, 102, 102, 102]\n", "✗ [100, 101, 102, 103, 103, 103, 103, 103, 103, 103]\n", "✗ [100, 101, 102, 103, 104, 104, 104, 104, 104, 104]\n", "✗ [100, 101, 102, 103, 104, 105, 105, 105, 105, 105]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 106, 106, 106]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 107, 107]\n", "✗ [100, 101, 102, 103, 104, 105, 106, 107, 108, 108]\n", "✓ [0, 101, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 0, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 102, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 0, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 1, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 3, 103, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 103, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 0, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 1, 104, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 2, 104, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 0, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 1, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 3, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 7, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 5, 105, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 105, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 0, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 1, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 3, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 7, 106, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 4, 106, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 0, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 1, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 3, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 7, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 5, 107, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 107, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 0, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 1, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 3, 108, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 5, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 6, 108, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 0, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 1, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 3, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 7, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 15, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 11, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 9, 109]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 6, 109]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 1]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 3]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 7]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 15]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 11]\n", "✓ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n", "✗ [0, 1, 2, 3, 4, 5, 6, 7, 8, 8]\n", "\n", "20 shrinks with 75 function calls\n" ] } ], "source": [ "show_trace(\n", " [100 + i for i in range(10)], lambda x: len(set(x)) >= 10, multicourse_shrink2\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This worked great! It saved us a huge number of function calls.\n", "\n", "Unfortunately it's wrong. Actually the previous one was wrong too, but this one is more obviously wrong. The problem is that shrinking later elements can unlock more shrinks for earlier elements and we'll never be able to benefit from that here:" ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [101, 100]\n", "✗ [101]\n", "✗ [100]\n", "✗ [100, 100]\n", "✗ [0, 100]\n", "✗ [1, 100]\n", "✗ [3, 100]\n", "✗ [7, 100]\n", "✗ [15, 100]\n", "✗ [31, 100]\n", "✗ [63, 100]\n", "✗ [82, 100]\n", "✗ [91, 100]\n", "✗ [96, 100]\n", "✗ [98, 100]\n", "✗ [99, 100]\n", "✓ [101, 0]\n", "\n", "1 shrinks with 16 function calls\n" ] } ], "source": [ "show_trace([101, 100], lambda x: len(x) >= 2 and x[0] > x[1], multicourse_shrink2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Armed with this example we can also show an example where the previous one is wrong because a later simplification unlocks an earlier one because shrinking values allows us to delete more elements:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [5, 5, 5, 5, 5, 5, 5, 5, 5, 5]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✓ [5, 5, 5, 5, 5, 5, 5, 5]\n", "✓ [0, 0, 0, 0, 0, 0, 0, 0]\n", "\n", "2 shrinks with 5 function calls\n" ] } ], "source": [ "show_trace([5] * 10, lambda x: x and len(x) > max(x), multicourse_shrink1)" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "collapsed": true }, "outputs": [], "source": [ "conditions[\"First > Second\"] = lambda xs: len(xs) >= 2 and xs[0] > xs[1]" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Note: We modify this to mask off the high bits because otherwise the probability of\n", "# hitting the condition at random is too low.\n", "conditions[\"Size > max & 63\"] = lambda xs: xs and len(xs) > (max(xs) & 63)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So what we'll try doing is iterating this to a fixed point and see what happens:" ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def multicourse_shrink3(ls, constraint):\n", " seen = set()\n", " while True:\n", " old_ls = ls\n", " for shrink in shrinkers_for(ls):\n", " while True:\n", " for s in shrink(ls):\n", " key = tuple(s)\n", " if key in seen:\n", " continue\n", " seen.add(key)\n", " if constraint(s):\n", " ls = s\n", " break\n", " else:\n", " break\n", " if ls == old_ls:\n", " return ls" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [101, 100]\n", "✗ [101]\n", "✗ [100]\n", "✗ [100, 100]\n", "✗ [0, 100]\n", "✗ [1, 100]\n", "✗ [3, 100]\n", "✗ [7, 100]\n", "✗ [15, 100]\n", "✗ [31, 100]\n", "✗ [63, 100]\n", "✗ [82, 100]\n", "✗ [91, 100]\n", "✗ [96, 100]\n", "✗ [98, 100]\n", "✗ [99, 100]\n", "✓ [101, 0]\n", "✗ [0]\n", "✗ [0, 0]\n", "✓ [1, 0]\n", "✗ [1]\n", "\n", "2 shrinks with 20 function calls\n" ] } ], "source": [ "show_trace([101, 100], lambda xs: len(xs) >= 2 and xs[0] > xs[1], multicourse_shrink3)" ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [5, 5, 5, 5, 5, 5, 5, 5, 5, 5]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✓ [5, 5, 5, 5, 5, 5, 5, 5]\n", "✓ [5, 5, 5, 5, 5, 5, 5]\n", "✓ [5, 5, 5, 5, 5, 5]\n", "✗ [5, 5, 5, 5, 5]\n", "✓ [0, 0, 0, 0, 0, 0]\n", "✓ [0]\n", "✗ []\n", "\n", "5 shrinks with 10 function calls\n" ] } ], "source": [ "show_trace([5] * 10, lambda x: x and len(x) > max(x), multicourse_shrink3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So that worked. Yay!\n", "\n", "Lets compare how this does to our single pass implementation." ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", "\n", "
ConditionSingle passMulti pass
length >= 2 6 6
sum >= 500 35 35
sum >= 3 6 6
At least 10 by 5 107 73
10 distinct elements 623 131
First > Second 1481 1445
Size > max & 63 600 > 5000
" ], "text/plain": [ "" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "compare_simplifiers(\n", " [\n", " (\"Single pass\", partial(greedy_shrink_with_dedupe, shrink=shrink6)),\n", " (\"Multi pass\", multicourse_shrink3),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So the answer is generally favourably but *ouch* that last one.\n", "\n", "What's happening there is that because later shrinks are opening up potentially very large improvements accessible to the lower shrinks, the original greedy algorithm can exploit that much better, while the multi pass algorithm spends a lot of time in the later stages with their incremental shrinks.\n", "\n", "Lets see another similar example before we try to fix this:" ] }, { "cell_type": "code", "execution_count": 43, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import hashlib\n", "\n", "conditions[\"Messy\"] = (\n", " lambda xs: hashlib.md5(repr(xs).encode(\"utf-8\")).hexdigest()[0] == \"0\"\n", ")" ] }, { "cell_type": "code", "execution_count": 44, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", "\n", "
ConditionSingle passMulti pass
length >= 2 6 6
sum >= 500 35 35
sum >= 3 6 6
At least 10 by 5 107 73
10 distinct elements 623 131
First > Second 1481 1445
Size > max & 63 600 > 5000
Messy 1032 > 5000
" ], "text/plain": [ "" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "compare_simplifiers(\n", " [\n", " (\"Single pass\", partial(greedy_shrink_with_dedupe, shrink=shrink6)),\n", " (\"Multi pass\", multicourse_shrink3),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This one is a bit different in that the problem is not that the structure is one we're ill suited to exploiting, it's that there is no structure at all so we have no hope of exploiting it. Literally any change at all will unlock earlier shrinks we could have done.\n", "\n", "What we're going to try to do is hybridize the two approaches. If we notice we're performing an awful lot of shrinks we can take that as a hint that we should be trying again from earlier stages.\n", "\n", "Here is our first approach. We simply restart the whole process every five shrinks:" ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "collapsed": true }, "outputs": [], "source": [ "MAX_SHRINKS_PER_RUN = 2\n", "\n", "\n", "def multicourse_shrink4(ls, constraint):\n", " seen = set()\n", " while True:\n", " old_ls = ls\n", " shrinks_this_run = 0\n", " for shrink in shrinkers_for(ls):\n", " while shrinks_this_run < MAX_SHRINKS_PER_RUN:\n", " for s in shrink(ls):\n", " key = tuple(s)\n", " if key in seen:\n", " continue\n", " seen.add(key)\n", " if constraint(s):\n", " shrinks_this_run += 1\n", " ls = s\n", " break\n", " else:\n", " break\n", " if ls == old_ls:\n", " return ls" ] }, { "cell_type": "code", "execution_count": 46, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", "\n", "
ConditionSingle passMulti passMulti pass with restart
length >= 2 6 6 6
sum >= 500 35 35 35
sum >= 3 6 6 6
At least 10 by 5 107 73 90
10 distinct elements 623 131 396
First > Second 1481 1445 1463
Size > max & 63 600 > 5000 > 5000
Messy 1032 > 5000 1423
" ], "text/plain": [ "" ] }, "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": [ "compare_simplifiers(\n", " [\n", " (\"Single pass\", partial(greedy_shrink_with_dedupe, shrink=shrink6)),\n", " (\"Multi pass\", multicourse_shrink3),\n", " (\"Multi pass with restart\", multicourse_shrink4),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That works OK, but it's pretty unsatisfying as it loses us most of the benefits of the multi pass shrinking - we're now at most twice as good as the greedy one.\n", "\n", "So what we're going to do is bet on the multi pass working and then gradually degrade to the greedy algorithm as it fails to work." ] }, { "cell_type": "code", "execution_count": 47, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def multicourse_shrink5(ls, constraint):\n", " seen = set()\n", " max_shrinks_per_run = 10\n", " while True:\n", " shrinks_this_run = 0\n", " for shrink in shrinkers_for(ls):\n", " while shrinks_this_run < max_shrinks_per_run:\n", " for s in shrink(ls):\n", " key = tuple(s)\n", " if key in seen:\n", " continue\n", " seen.add(key)\n", " if constraint(s):\n", " shrinks_this_run += 1\n", " ls = s\n", " break\n", " else:\n", " break\n", " if max_shrinks_per_run > 1:\n", " max_shrinks_per_run -= 2\n", " if not shrinks_this_run:\n", " return ls" ] }, { "cell_type": "code", "execution_count": 48, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ [5, 5, 5, 5, 5, 5, 5, 5, 5, 5]\n", "✗ [5]\n", "✗ [5, 5]\n", "✗ [5, 5, 5, 5]\n", "✓ [5, 5, 5, 5, 5, 5, 5, 5]\n", "✓ [5, 5, 5, 5, 5, 5, 5]\n", "✓ [5, 5, 5, 5, 5, 5]\n", "✗ [5, 5, 5, 5, 5]\n", "✓ [0, 0, 0, 0, 0, 0]\n", "✓ [0]\n", "✗ []\n", "\n", "5 shrinks with 10 function calls\n" ] } ], "source": [ "show_trace([5] * 10, lambda x: x and len(x) > max(x), multicourse_shrink5)" ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", " \n", "\n", "\n", "\n", "
ConditionSingle passMulti passMulti pass with restartMulti pass with variable restart
length >= 2 6 6 6 6
sum >= 500 35 35 35 35
sum >= 3 6 6 6 6
At least 10 by 5 107 73 90 73
10 distinct elements 623 131 396 212
First > Second 1481 1445 1463 1168
Size > max & 63 600 > 5000 > 5000 1002
Messy 1032 > 5000 1423 824
" ], "text/plain": [ "" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "compare_simplifiers(\n", " [\n", " (\"Single pass\", partial(greedy_shrink_with_dedupe, shrink=shrink6)),\n", " (\"Multi pass\", multicourse_shrink3),\n", " (\"Multi pass with restart\", multicourse_shrink4),\n", " (\"Multi pass with variable restart\", multicourse_shrink5),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is now more or less the current state of the art (it's actually a bit different from the Hypothesis state of the art at the time of this writing. I'm planning to merge some of the things I figured out in the course of writing this back in). We've got something that is able to adaptively take advantage of structure where it is present, but degrades reasonably gracefully back to the more aggressive version that works better in unstructured examples.\n", "\n", "Surprisingly, on some examples it seems to even be best of all of them. I think that's more coincidence than truth though." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.4.3" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: paper.bib ================================================ @inproceedings{DBLP:conf/icfp/ClaessenH00, author = {Koen Claessen and John Hughes}, title = {QuickCheck: a lightweight tool for random testing of {H}askell programs}, booktitle = {{Proceedings of the Fifth ACM SIGPLAN International Conference on Functional Programming (ICFP '00)}}, pages = {268--279}, year = {2000}, crossref = {DBLP:conf/icfp/2000}, url = {https://doi.org/10.1145/351240.351266}, doi = {10.1145/351240.351266}, timestamp = {Tue, 06 Nov 2018 16:59:25 +0100}, biburl = {https://dblp.org/rec/bib/conf/icfp/ClaessenH00}, bibsource = {dblp computer science bibliography, https://dblp.org} } @proceedings{DBLP:conf/icfp/2000, editor = {Martin Odersky and Philip Wadler}, title = {{Proceedings of the Fifth ACM SIGPLAN International Conference on Functional Programming (ICFP '00)}}, publisher = {{ACM}}, year = {2000}, isbn = {1-58113-202-6}, timestamp = {Tue, 11 Jun 2013 13:51:25 +0200}, biburl = {https://dblp.org/rec/bib/conf/icfp/2000}, bibsource = {dblp computer science bibliography, https://dblp.org} } @inproceedings{DBLP:conf/erlang/ArtsHJW06, author = {Thomas Arts and John Hughes and Joakim Johansson and Ulf T. Wiger}, title = {Testing telecoms software with quviq {QuickCheck}}, booktitle = {{Proceedings of the 2006 ACM SIGPLAN Workshop on Erlang}}, pages = {2--10}, year = {2006}, crossref = {DBLP:conf/erlang/2006}, url = {https://doi.org/10.1145/1159789.1159792}, doi = {10.1145/1159789.1159792}, timestamp = {Tue, 06 Nov 2018 16:59:37 +0100}, biburl = {https://dblp.org/rec/bib/conf/erlang/ArtsHJW06}, bibsource = {dblp computer science bibliography, https://dblp.org} } @proceedings{DBLP:conf/erlang/2006, editor = {Marc Feeley and Philip W. Trinder}, title = {{Proceedings of the 2006 ACM SIGPLAN Workshop on Erlang}}, publisher = {{ACM}}, year = {2006}, isbn = {1-59593-490-1}, timestamp = {Wed, 02 Apr 2008 10:59:25 +0200}, biburl = {https://dblp.org/rec/bib/conf/erlang/2006}, bibsource = {dblp computer science bibliography, https://dblp.org} } @misc{PraiseOfPBT, title = {In Praise of Property-Based Testing}, howpublished = {\url{https://increment.com/testing/in-praise-of-property-based-testing/}}, year={2019}, author={David R. MacIver}, } @article{DBLP:journals/tse/ZellerH02, author = {Andreas Zeller and Ralf Hildebrandt}, title = {Simplifying and Isolating Failure-Inducing Input}, journal = {{IEEE} Trans. Software Eng.}, volume = {28}, number = {2}, pages = {183--200}, year = {2002}, url = {https://doi.org/10.1109/32.988498}, doi = {10.1109/32.988498}, timestamp = {Wed, 14 Nov 2018 10:49:20 +0100}, biburl = {https://dblp.org/rec/bib/journals/tse/ZellerH02}, bibsource = {dblp computer science bibliography, https://dblp.org} } @inproceedings{DBLP:conf/pldi/RegehrCCEEY12, author = {John Regehr and Yang Chen and Pascal Cuoq and Eric Eide and Chucky Ellison and Xuejun Yang}, title = {Test-case reduction for {C} compiler bugs}, booktitle = {{ACM SIGPLAN Conference on Programming Language Design and Implementation, (PLDI '12)}}, pages = {335--346}, year = {2012}, crossref = {DBLP:conf/pldi/2012}, url = {https://doi.org/10.1145/2254064.2254104}, doi = {10.1145/2254064.2254104}, timestamp = {Wed, 14 Nov 2018 10:54:59 +0100}, biburl = {https://dblp.org/rec/bib/conf/pldi/RegehrCCEEY12}, bibsource = {dblp computer science bibliography, https://dblp.org} } @proceedings{DBLP:conf/pldi/2012, editor = {Jan Vitek and Haibo Lin and Frank Tip}, title = {{ACM SIGPLAN Conference on Programming Language Design and Implementation, (PLDI '12)}}, publisher = {{ACM}}, year = {2012}, url = {http://dl.acm.org/citation.cfm?id=2254064}, isbn = {978-1-4503-1205-9}, timestamp = {Tue, 12 Jun 2012 19:17:55 +0200}, biburl = {https://dblp.org/rec/bib/conf/pldi/2012}, bibsource = {dblp computer science bibliography, https://dblp.org} } @article{astropy:2013, Adsnote = {Provided by the SAO/NASA Astrophysics Data System}, Adsurl = {http://adsabs.harvard.edu/abs/2013A%26A...558A..33A}, Archiveprefix = {arXiv}, Author = {{Astropy Collaboration} and {Robitaille}, T.~P. and {Tollerud}, E.~J. and {Greenfield}, P. and {Droettboom}, M. and {Bray}, E. and {Aldcroft}, T. and {Davis}, M. and {Ginsburg}, A. and {Price-Whelan}, A.~M. and {Kerzendorf}, W.~E. and {Conley}, A. and {Crighton}, N. and {Barbary}, K. and {Muna}, D. and {Ferguson}, H. and {Grollier}, F. and {Parikh}, M.~M. and {Nair}, P.~H. and {Unther}, H.~M. and {Deil}, C. and {Woillez}, J. and {Conseil}, S. and {Kramer}, R. and {Turner}, J.~E.~H. and {Singer}, L. and {Fox}, R. and {Weaver}, B.~A. and {Zabalza}, V. and {Edwards}, Z.~I. and {Azalee Bostroem}, K. and {Burke}, D.~J. and {Casey}, A.~R. and {Crawford}, S.~M. and {Dencheva}, N. and {Ely}, J. and {Jenness}, T. and {Labrie}, K. and {Lim}, P.~L. and {Pierfederici}, F. and {Pontzen}, A. and {Ptak}, A. and {Refsdal}, B. and {Servillat}, M. and {Streicher}, O.}, Doi = {10.1051/0004-6361/201322068}, Eid = {A33}, Eprint = {1307.6212}, Journal = {\aap}, Keywords = {methods: data analysis, methods: miscellaneous, virtual observatory tools}, Month = oct, Pages = {A33}, Primaryclass = {astro-ph.IM}, Title = {{Astropy: A community Python package for astronomy}}, Volume = 558, Year = 2013, Bdsk-Url-1 = {https://dx.doi.org/10.1051/0004-6361/201322068}} @article{astropy:2018, Adsnote = {Provided by the SAO/NASA Astrophysics Data System}, Adsurl = {https://ui.adsabs.harvard.edu/#abs/2018AJ....156..123T}, Author = {{Price-Whelan}, A.~M. and {Sip{\H{o}}cz}, B.~M. and {G{\"u}nther}, H.~M. and {Lim}, P.~L. and {Crawford}, S.~M. and {Conseil}, S. and {Shupe}, D.~L. and {Craig}, M.~W. and {Dencheva}, N. and {Ginsburg}, A. and {VanderPlas}, J.~T. and {Bradley}, L.~D. and {P{\'e}rez-Su{\'a}rez}, D. and {de Val-Borro}, M. and {Paper Contributors}, (Primary and {Aldcroft}, T.~L. and {Cruz}, K.~L. and {Robitaille}, T.~P. and {Tollerud}, E.~J. and {Coordination Committee}, (Astropy and {Ardelean}, C. and {Babej}, T. and {Bach}, Y.~P. and {Bachetti}, M. and {Bakanov}, A.~V. and {Bamford}, S.~P. and {Barentsen}, G. and {Barmby}, P. and {Baumbach}, A. and {Berry}, K.~L. and {Biscani}, F. and {Boquien}, M. and {Bostroem}, K.~A. and {Bouma}, L.~G. and {Brammer}, G.~B. and {Bray}, E.~M. and {Breytenbach}, H. and {Buddelmeijer}, H. and {Burke}, D.~J. and {Calderone}, G. and {Cano Rodr{\'\i}guez}, J.~L. and {Cara}, M. and {Cardoso}, J.~V.~M. and {Cheedella}, S. and {Copin}, Y. and {Corrales}, L. and {Crichton}, D. and {D{\textquoteright}Avella}, D. and {Deil}, C. and {Depagne}, {\'E}. and {Dietrich}, J.~P. and {Donath}, A. and {Droettboom}, M. and {Earl}, N. and {Erben}, T. and {Fabbro}, S. and {Ferreira}, L.~A. and {Finethy}, T. and {Fox}, R.~T. and {Garrison}, L.~H. and {Gibbons}, S.~L.~J. and {Goldstein}, D.~A. and {Gommers}, R. and {Greco}, J.~P. and {Greenfield}, P. and {Groener}, A.~M. and {Grollier}, F. and {Hagen}, A. and {Hirst}, P. and {Homeier}, D. and {Horton}, A.~J. and {Hosseinzadeh}, G. and {Hu}, L. and {Hunkeler}, J.~S. and {Ivezi{\'c}}, {\v{Z}}. and {Jain}, A. and {Jenness}, T. and {Kanarek}, G. and {Kendrew}, S. and {Kern}, N.~S. and {Kerzendorf}, W.~E. and {Khvalko}, A. and {King}, J. and {Kirkby}, D. and {Kulkarni}, A.~M. and {Kumar}, A. and {Lee}, A. and {Lenz}, D. and {Littlefair}, S.~P. and {Ma}, Z. and {Macleod}, D.~M. and {Mastropietro}, M. and {McCully}, C. and {Montagnac}, S. and {Morris}, B.~M. and {Mueller}, M. and {Mumford}, S.~J. and {Muna}, D. and {Murphy}, N.~A. and {Nelson}, S. and {Nguyen}, G.~H. and {Ninan}, J.~P. and {N{\"o}the}, M. and {Ogaz}, S. and {Oh}, S. and {Parejko}, J.~K. and {Parley}, N. and {Pascual}, S. and {Patil}, R. and {Patil}, A.~A. and {Plunkett}, A.~L. and {Prochaska}, J.~X. and {Rastogi}, T. and {Reddy Janga}, V. and {Sabater}, J. and {Sakurikar}, P. and {Seifert}, M. and {Sherbert}, L.~E. and {Sherwood-Taylor}, H. and {Shih}, A.~Y. and {Sick}, J. and {Silbiger}, M.~T. and {Singanamalla}, S. and {Singer}, L.~P. and {Sladen}, P.~H. and {Sooley}, K.~A. and {Sornarajah}, S. and {Streicher}, O. and {Teuben}, P. and {Thomas}, S.~W. and {Tremblay}, G.~R. and {Turner}, J.~E.~H. and {Terr{\'o}n}, V. and {van Kerkwijk}, M.~H. and {de la Vega}, A. and {Watkins}, L.~L. and {Weaver}, B.~A. and {Whitmore}, J.~B. and {Woillez}, J. and {Zabalza}, V. and {Contributors}, (Astropy}, Doi = {10.3847/1538-3881/aabc4f}, Eid = {123}, Journal = {\aj}, Keywords = {methods: data analysis, methods: miscellaneous, methods: statistical, reference systems, Astrophysics - Instrumentation and Methods for Astrophysics}, Month = Sep, Pages = {123}, Primaryclass = {astro-ph.IM}, Title = {{The Astropy Project: Building an Open-science Project and Status of the v2.0 Core Package}}, Volume = {156}, Year = 2018, Bdsk-Url-1 = {https://doi.org/10.3847/1538-3881/aabc4f}} @article{DBLP:journals/cse/WaltCV11, author = {St{\'{e}}fan van der Walt and S. Chris Colbert and Ga{\"{e}}l Varoquaux}, title = {The {NumPy} Array: {A} Structure for Efficient Numerical Computation}, journal = {Computing in Science and Engineering}, volume = {13}, number = {2}, pages = {22--30}, year = {2011}, url = {https://doi.org/10.1109/MCSE.2011.37}, doi = {10.1109/MCSE.2011.37}, timestamp = {Wed, 14 Nov 2018 10:48:31 +0100}, biburl = {https://dblp.org/rec/bib/journals/cse/WaltCV11}, bibsource = {dblp computer science bibliography, https://dblp.org} } @inproceedings{DBLP:conf/issta/LoscherS17, author = {Andreas L{\"{o}}scher and Konstantinos Sagonas}, title = {Targeted property-based testing}, booktitle = {{Proceedings of the 26th ACM SIGSOFT International Symposium on Software Testing and Analysis}}, pages = {46--56}, year = {2017}, crossref = {DBLP:conf/issta/2017}, url = {https://doi.org/10.1145/3092703.3092711}, doi = {10.1145/3092703.3092711}, timestamp = {Wed, 25 Sep 2019 18:08:21 +0200}, biburl = {https://dblp.org/rec/bib/conf/issta/LoscherS17}, bibsource = {dblp computer science bibliography, https://dblp.org} } @proceedings{DBLP:conf/issta/2017, editor = {Tevfik Bultan and Koushik Sen}, title = {{Proceedings of the 26th ACM SIGSOFT International Symposium on Software Testing and Analysis}}, publisher = {{ACM}}, year = {2017}, url = {https://doi.org/10.1145/3092703}, doi = {10.1145/3092703}, isbn = {978-1-4503-5076-1}, timestamp = {Tue, 06 Nov 2018 16:57:30 +0100}, biburl = {https://dblp.org/rec/bib/conf/issta/2017}, bibsource = {dblp computer science bibliography, https://dblp.org} } ================================================ FILE: paper.md ================================================ --- title: 'Hypothesis: A new approach to property-based testing' date: 1 November 2019 bibliography: paper.bib tags: - Python - testing - test-case reduction - test-case generation - property-based testing authors: - name: David R. MacIver orcid: 0000-0002-8635-3223 affiliation: 1 - name: Zac Hatfield-Dodds orcid: 0000-0002-8646-8362 affiliation: 2 - name: many other contributors affiliation: 3 affiliations: - name: Imperial College London index: 1 - name: Australian National University index: 2 - name: Various index: 3 --- # Summary *Property-based testing* is a style of testing popularised by the QuickCheck family of libraries, first in Haskell [@DBLP:conf/icfp/ClaessenH00] and later in Erlang [@DBLP:conf/erlang/ArtsHJW06], which integrates generated test cases into existing software testing workflows: Instead of tests that provide examples of a single concrete behaviour, tests specify properties that hold for a wide range of inputs, which the testing library then attempts to generate test cases to refute. For a general introduction to property-based testing, see [@PraiseOfPBT]. Hypothesis is a mature and widely used property-based testing library for Python. It has over 100,000 downloads per week^[https://pypistats.org/packages/hypothesis], thousands of open source projects use it^[https://github.com/HypothesisWorks/hypothesis/network/dependents], and in 2018 more than 4% of Python users surveyed by the PSF reported using it^[https://www.jetbrains.com/research/python-developers-survey-2018/]. It will be of interest both to researchers using Python for developing scientific software, and to software testing researchers as a platform for research in its own right. # Hypothesis for Testing Scientific Software Python has a rich and thriving ecosystem of scientific software, and Hypothesis is helpful for ensuring its correctness. Any researcher who tests their software in Python can benefit from these facilities, but it is particularly useful for improving the correctness foundational libraries on which the scientific software ecosystem is built. For example, it has found bugs in astropy [@astropy:2018]^[e.g. https://github.com/astropy/astropy/pull/9328, https://github.com/astropy/astropy/pull/9532] and numpy [@DBLP:journals/cse/WaltCV11]^[e.g. https://github.com/numpy/numpy/issues/10930, https://github.com/numpy/numpy/issues/13089, https://github.com/numpy/numpy/issues/14239]. Additionally, Hypothesis is easily extensible, and has a number of third-party extensions for specific research applications. For example, hypothesis-networkx^[https://pypi.org/project/hypothesis-networkx/] generates graph data structures, and hypothesis-bio^[https://pypi.org/project/hypothesis-bio/] generates formats suitable for bioinformatics. As it is used by more researchers, the number of research applications will only increase. By lowering the barrier to effective testing, Hypothesis makes testing of research software written in Python much more compelling, and has the potential to significantly improve the quality of the associated scientific research as a result. # Hypothesis for Software Testing Research Hypothesis is a powerful platform for software testing research, both because of the wide array of software that can be easily tested with it, and because it has a novel implementation that solves a major difficulty faced by prior software testing research. Much of software testing research boils down to variants on the following problem: Given some interestingness condition (e.g., that it triggers a bug in some software), how do we generate a "good" test case that satisfies that condition? Particular sub-problems of this are: 1. How do we generate test cases that satisfy difficult interestingness conditions? 2. How do we ensure we generate only valid test cases? (the *test-case validity problem* - see @DBLP:conf/pldi/RegehrCCEEY12) 3. How do we generate human readable test cases? Traditionally property-based testing has adopted random test-case generation to find interesting test cases, followed by test-case reduction (see @DBLP:conf/pldi/RegehrCCEEY12, @DBLP:journals/tse/ZellerH02) to turn them into more human readable ones, requiring the users to manually specify a *validity oracle* (a predicate that identifies if an arbitrary test case is valid) to avoid invalid test cases. The chief limitations of this from a user's point of view are: * Writing correct validity oracles is difficult and annoying. * Random generation, while often much better than hand-written examples, is not especially good at satisfying difficult properties. * Writing test-case reducers that work well for your problem domain is a specialised skill that few people have or want to acquire. The chief limitation from a researcher's point of view is that trying to improve on random generation's ability to find bugs will typically require modification of existing tests to support new ways of generating data, and typically these modifications are significantly more complex than writing the random generator would have been. Users are rarely going to be willing to undertake the work themselves, which leaves researchers in the unfortunate position of having to put in a significant amount of work per project to understand how to test it. Hypothesis avoids both of these problems by using a single universal representation for test cases. Ensuring that test cases produced from this format are valid is relatively easy, no more difficult than ensuring that randomly generated tests cases are valid, and improvements to the generation process can operate solely on this universal representation rather than requiring adapting to each test. Currently Hypothesis uses this format to support two major use cases: 1. It is the basis of its approach to test-case reduction, allowing it to support more powerful test-case reduction than is found in most property-based testing libraries with no user intervention. 2. It supports Targeted Property-Based Testing [@DBLP:conf/issta/LoscherS17], which uses a score to guide testing towards a particular goal (e.g., maximising an error term). In the original implementation this would require custom mutation operators per test, but in Hypothesis this mutation is transparent to the user and they need only specify the goal. The internal format is flexible and contains rich information about the structure of generated test cases, so it is likely future versions of the software will see other features built on top of it, and we hope researchers will use it as a vehicle to explore other interesting possibilities for test-case generation. # References ================================================ FILE: pyproject.toml ================================================ [tool.ruff] line-length = 88 target-version = "py310" [tool.ruff.per-file-target-version] "test_typealias_py312.py" = "py312" [tool.ruff.lint] select = [ "ASYNC", # flake8-async "B", # flake8-bugbear "C4", # flake8-comprehensions "COM", # flake8-commas "DJ", # flake8-django "E", # pycodestyle "F", # Pyflakes "FBT", # flake8-boolean-trap "FLY", # flynt "G", # flake8-logging-format "INT", # flake8-gettext "ISC", # flake8-implicit-str-concat "NPY", # NumPy-specific rules "PD", # pandas-vet "PIE", # flake8-pie "PLE", # Pylint errors "PT", # flake8-pytest-style "RET504", # flake8-return "RSE", # flake8-raise "SIM", # flake8-simplify "T10", # flake8-debugger "TID", # flake8-tidy-imports "UP", # pyupgrade "W", # pycodestyle "YTT", # flake8-2020 "RUF", # Ruff-specific rules ] ignore = [ "B008", "B018", "C408", "C420", "COM812", "DJ007", "DJ008", # "line too long". ruff uses tool.ruff.line-length both for this rule, and for `ruff format`. # We want ruff format to match black's 88 line length, so we set tool.ruff.line-length = 88. But # this is a formatter suggestion, not a rule, so we also disable the E501 lint. "E501", "E721", "E731", "E741", "FBT001", "FBT003", "PD011", "PIE790", # See https://github.com/astral-sh/ruff/issues/10538 "PT001", "PT003", "PT006", "PT007", "PT009", "PT011", "PT012", "PT013", "PT015", "PT017", "PT019", "PT023", "PT027", "PT031", "RUF001", # don't break our tests by rewriting confusables "RUF005", "RUF017", "RUF028", # we use fmt: off for black as well as ruff, with differing semantics "SIM102", "SIM105", "SIM108", "SIM114", "SIM300", "SIM905", "UP031", "UP037", ] [tool.ruff.lint.per-file-ignores] "hypothesis-python/src/hypothesis/core.py" = ["B030", "B904"] "hypothesis-python/src/hypothesis/internal/compat.py" = ["F401"] "hypothesis-python/tests/nocover/test_imports.py" = ["F403", "F405"] "hypothesis-python/tests/numpy/test_randomness.py" = ["NPY002"] "hypothesis-python/src/hypothesis/internal/conjecture/*" = ["B023"] "hypothesis-python/tests/conjecture/test_data_tree.py" = ["B023"] [tool.mypy] python_version = "3.14" platform = "linux" allow_redefinition = true disallow_untyped_decorators = true disallow_incomplete_defs = true no_implicit_optional = true no_implicit_reexport = true follow_imports = "silent" ignore_missing_imports = true strict_equality = true warn_no_return = true warn_unused_ignores = true warn_unused_configs = true warn_redundant_casts = true disable_error_code = "annotation-unchecked" # pytest 9 added support for [tool.pytest], which we should move to when pytest 9 # is the oldest one we support. [tool.pytest.ini_options] # -rfEX :: Print a summary of failures, errors, and xpasses (xfails that pass). addopts = "-rfEX --strict-markers --tb=native -p pytester -p no:legacypath --runpytest=subprocess --durations=20 --durations-min=1.0" xfail_strict = true filterwarnings = [ "error", # https://github.com/pandas-dev/pandas/issues/41199 "default:Creating a LegacyVersion has been deprecated and will be removed in the next major release:DeprecationWarning", "default:distutils Version classes are deprecated\\. Use packaging\\.version instead:DeprecationWarning", # https://github.com/pandas-dev/pandas/issues/32056 (?) "default:numpy\\.ufunc size changed, may indicate binary incompatibility\\. Expected 216 from C header, got 232 from PyObject:RuntimeWarning", # https://github.com/pandas-dev/pandas/issues/34848 "default:`np\\.bool` is a deprecated alias for the builtin `bool`:DeprecationWarning", "default:`np\\.complex` is a deprecated alias for the builtin `complex`:DeprecationWarning", "default:`np\\.object` is a deprecated alias for the builtin `object`:DeprecationWarning", # pytest-cov can't see into subprocesses; we'll see <100% covered if this is an issue "ignore:Module hypothesis.* was previously imported, but not measured", "ignore:CrosshairPrimitiveProvider.realize does not have the for_failure parameter", ] ================================================ FILE: requirements/coverage.in ================================================ annotated-types black click dpcontracts fakeredis lark libcst numpy pandas pyarrow # Silence warning from Pandas >=2.2, <3 - see https://github.com/pandas-dev/pandas/issues/54466 pytest-cov python-dateutil pytz typing-extensions watchdog -r test.in ================================================ FILE: requirements/coverage.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # ./build.sh upgrade-requirements # annotated-types==0.7.0 # via -r requirements/coverage.in black==26.3.1 # via -r requirements/coverage.in click==8.3.1 # via # -r requirements/coverage.in # black coverage[toml]==7.13.4 # via pytest-cov dpcontracts==0.6.0 # via -r requirements/coverage.in execnet==2.1.2 # via pytest-xdist fakeredis==2.34.1 # via -r requirements/coverage.in iniconfig==2.3.0 # via pytest lark==1.3.1 # via -r requirements/coverage.in libcst==1.8.6 # via -r requirements/coverage.in mypy-extensions==1.1.0 # via black numpy==2.4.3 # via # -r requirements/coverage.in # pandas packaging==26.0 # via # black # pytest pandas==3.0.1 # via -r requirements/coverage.in pathspec==1.0.4 # via black pexpect==4.9.0 # via -r requirements/test.in platformdirs==4.9.4 # via black pluggy==1.6.0 # via # pytest # pytest-cov ptyprocess==0.7.0 # via pexpect pyarrow==23.0.1 # via -r requirements/coverage.in pygments==2.19.2 # via pytest pytest==9.0.2 # via # -r requirements/test.in # pytest-cov # pytest-xdist pytest-cov==7.0.0 # via -r requirements/coverage.in pytest-xdist==3.8.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via # -r requirements/coverage.in # pandas pytokens==0.4.1 # via black pytz==2026.1.post1 # via -r requirements/coverage.in pyyaml==6.0.3 # via libcst redis==7.3.0 # via fakeredis six==1.17.0 # via python-dateutil sortedcontainers==2.4.0 # via # fakeredis # hypothesis (hypothesis-python/pyproject.toml) typing-extensions==4.15.0 # via -r requirements/coverage.in watchdog==6.0.0 # via -r requirements/coverage.in ================================================ FILE: requirements/crosshair.in ================================================ crosshair-tool hypothesis-crosshair -r test.in ================================================ FILE: requirements/crosshair.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # ./build.sh upgrade-requirements # attrs==25.4.0 # via # cattrs # lsprotocol # pygls cattrs==26.1.0 # via # lsprotocol # pygls crosshair-tool==0.0.102 # via # -r requirements/crosshair.in # hypothesis-crosshair execnet==2.1.2 # via pytest-xdist hypothesis==6.151.9 # via hypothesis-crosshair hypothesis-crosshair==0.0.27 # via -r requirements/crosshair.in importlib-metadata==8.7.1 # via crosshair-tool importlib-resources==6.5.2 # via typeshed-client iniconfig==2.3.0 # via pytest lsprotocol==2025.0.0 # via pygls mypy-extensions==1.1.0 # via typing-inspect packaging==26.0 # via # crosshair-tool # pytest pexpect==4.9.0 # via -r requirements/test.in pluggy==1.6.0 # via pytest ptyprocess==0.7.0 # via pexpect pygls==2.0.1 # via crosshair-tool pygments==2.19.2 # via pytest pytest==9.0.2 # via # -r requirements/test.in # pytest-xdist pytest-xdist==3.8.0 # via -r requirements/test.in sortedcontainers==2.4.0 # via # hypothesis # hypothesis (hypothesis-python/pyproject.toml) typeshed-client==2.9.0 # via crosshair-tool typing-extensions==4.15.0 # via # cattrs # crosshair-tool # typeshed-client # typing-inspect typing-inspect==0.9.0 # via crosshair-tool z3-solver==4.16.0.0 # via crosshair-tool zipp==3.23.0 # via importlib-metadata ================================================ FILE: requirements/fuzzing.in ================================================ hypofuzz -r coverage.in ================================================ FILE: requirements/fuzzing.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # ./build.sh upgrade-requirements # annotated-types==0.7.0 # via -r requirements/coverage.in anyio==4.12.1 # via starlette attrs==25.4.0 # via # outcome # trio black==26.3.1 # via # -r requirements/coverage.in # hypofuzz # hypothesis click==8.3.1 # via # -r requirements/coverage.in # black # hypothesis coverage[toml]==7.13.4 # via # hypofuzz # pytest-cov dpcontracts==0.6.0 # via -r requirements/coverage.in execnet==2.1.2 # via pytest-xdist fakeredis==2.34.1 # via -r requirements/coverage.in h11==0.16.0 # via # hypercorn # wsproto h2==4.3.0 # via hypercorn hpack==4.1.0 # via h2 hypercorn==0.18.0 # via hypofuzz hyperframe==6.1.0 # via h2 hypofuzz==25.11.1 # via -r requirements/fuzzing.in hypothesis[cli,watchdog]==6.151.9 # via hypofuzz idna==3.11 # via # anyio # trio iniconfig==2.3.0 # via pytest lark==1.3.1 # via -r requirements/coverage.in libcst==1.8.6 # via # -r requirements/coverage.in # hypofuzz markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py mypy-extensions==1.1.0 # via black numpy==2.4.3 # via # -r requirements/coverage.in # pandas outcome==1.3.0.post0 # via trio packaging==26.0 # via # black # pytest pandas==3.0.1 # via -r requirements/coverage.in pathspec==1.0.4 # via black pexpect==4.9.0 # via -r requirements/test.in platformdirs==4.9.4 # via black pluggy==1.6.0 # via # pytest # pytest-cov priority==2.0.0 # via hypercorn psutil==7.2.2 # via hypofuzz ptyprocess==0.7.0 # via pexpect pyarrow==23.0.1 # via -r requirements/coverage.in pygments==2.19.2 # via # pytest # rich pytest==9.0.2 # via # -r requirements/test.in # hypofuzz # pytest-cov # pytest-xdist pytest-cov==7.0.0 # via -r requirements/coverage.in pytest-xdist==3.8.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via # -r requirements/coverage.in # pandas pytokens==0.4.1 # via black pytz==2026.1.post1 # via -r requirements/coverage.in pyyaml==6.0.3 # via libcst redis==7.3.0 # via fakeredis rich==14.3.3 # via hypothesis six==1.17.0 # via python-dateutil sniffio==1.3.1 # via trio sortedcontainers==2.4.0 # via # fakeredis # hypothesis # hypothesis (hypothesis-python/pyproject.toml) # trio starlette==0.52.1 # via hypofuzz trio==0.33.0 # via hypofuzz typing-extensions==4.15.0 # via -r requirements/coverage.in watchdog==6.0.0 # via # -r requirements/coverage.in # hypothesis wsproto==1.3.2 # via hypercorn ================================================ FILE: requirements/test.in ================================================ pexpect pytest pytest-xdist ================================================ FILE: requirements/test.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # ./build.sh upgrade-requirements # execnet==2.1.2 # via pytest-xdist iniconfig==2.3.0 # via pytest packaging==26.0 # via pytest pexpect==4.9.0 # via -r requirements/test.in pluggy==1.6.0 # via pytest ptyprocess==0.7.0 # via pexpect pygments==2.19.2 # via pytest pytest==9.0.2 # via # -r requirements/test.in # pytest-xdist pytest-xdist==3.8.0 # via -r requirements/test.in sortedcontainers==2.4.0 # via hypothesis (hypothesis-python/pyproject.toml) ================================================ FILE: requirements/tools.in ================================================ codespell coverage django dpcontracts ipython lark libcst mypy numpy pelican[markdown] pip-tools pyright python-dateutil requests restructuredtext-lint ruff shed==2024.1.1 sphinx sphinx-autobuild sphinx-codeautolink sphinx-jsonschema furo sphinx-selective-exclude tox twine types-click types-pytz types-redis typing-extensions watchdog # for typing build sortedcontainers-stubs # for typing attrs # for typing tomli # for update_pyproject_toml -r test.in ================================================ FILE: requirements/tools.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # ./build.sh upgrade-requirements # accessible-pygments==0.0.5 # via furo alabaster==1.0.0 # via sphinx anyio==4.12.1 # via # starlette # watchfiles asgiref==3.11.1 # via django asttokens==3.0.1 # via stack-data attrs==25.4.0 # via -r requirements/tools.in autoflake==2.3.3 # via shed babel==2.18.0 # via sphinx beautifulsoup4==4.14.3 # via # furo # sphinx-codeautolink black==26.3.1 # via shed blinker==1.9.0 # via pelican build==1.4.0 # via # -r requirements/tools.in # pip-tools cachetools==7.0.5 # via tox certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography charset-normalizer==3.4.5 # via requests click==8.3.1 # via # black # pip-tools # uvicorn codespell==2.4.2 # via -r requirements/tools.in colorama==0.4.6 # via # sphinx-autobuild # tox com2ann==0.3.0 # via shed coverage==7.13.4 # via -r requirements/tools.in cryptography==46.0.5 # via # secretstorage # types-pyopenssl # types-redis decorator==5.2.1 # via ipython distlib==0.4.0 # via virtualenv django==6.0.3 # via -r requirements/tools.in docutils==0.22.4 # via # pelican # readme-renderer # restructuredtext-lint # sphinx # sphinx-jsonschema dpcontracts==0.6.0 # via -r requirements/tools.in execnet==2.1.2 # via pytest-xdist executing==2.2.1 # via stack-data feedgenerator==2.2.1 # via pelican filelock==3.25.2 # via # python-discovery # tox # virtualenv furo==2025.12.19 # via -r requirements/tools.in h11==0.16.0 # via uvicorn id==1.6.1 # via twine idna==3.11 # via # anyio # requests imagesize==2.0.0 # via sphinx iniconfig==2.3.0 # via pytest ipython==9.11.0 # via -r requirements/tools.in ipython-pygments-lexers==1.1.1 # via ipython isort==8.0.1 # via shed jaraco-classes==3.4.0 # via keyring jaraco-context==6.1.1 # via keyring jaraco-functools==4.4.0 # via keyring jedi==0.19.2 # via ipython jeepney==0.9.0 # via # keyring # secretstorage jinja2==3.1.6 # via # pelican # sphinx jsonpointer==3.0.0 # via sphinx-jsonschema keyring==25.7.0 # via twine lark==1.3.1 # via -r requirements/tools.in libcst==1.8.6 # via # -r requirements/tools.in # shed librt==0.8.1 # via mypy markdown==3.10.2 # via pelican markdown-it-py==4.0.0 # via rich markupsafe==3.0.3 # via jinja2 matplotlib-inline==0.2.1 # via ipython mdurl==0.1.2 # via markdown-it-py more-itertools==10.8.0 # via # jaraco-classes # jaraco-functools mypy==1.19.1 # via -r requirements/tools.in mypy-extensions==1.1.0 # via # black # mypy nh3==0.3.3 # via readme-renderer nodeenv==1.10.0 # via pyright numpy==2.4.3 # via -r requirements/tools.in ordered-set==4.1.0 # via pelican packaging==26.0 # via # black # build # pyproject-api # pytest # sphinx # tox # twine # wheel parso==0.8.6 # via jedi pathspec==1.0.4 # via # black # mypy pelican[markdown]==4.11.0.post0 # via -r requirements/tools.in pexpect==4.9.0 # via # -r requirements/test.in # ipython pip-tools==7.5.3 # via -r requirements/tools.in platformdirs==4.9.4 # via # black # python-discovery # tox # virtualenv pluggy==1.6.0 # via # pytest # tox prompt-toolkit==3.0.52 # via ipython ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data pycparser==3.0 # via cffi pyflakes==3.4.0 # via autoflake pygments==2.18.0 # via # accessible-pygments # furo # ipython # ipython-pygments-lexers # pelican # pytest # readme-renderer # rich # sphinx pyproject-api==1.10.0 # via tox pyproject-hooks==1.2.0 # via # build # pip-tools pyright==1.1.408 # via -r requirements/tools.in pytest==9.0.2 # via # -r requirements/test.in # pytest-xdist pytest-xdist==3.8.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via # -r requirements/tools.in # pelican python-discovery==1.1.3 # via virtualenv pytokens==0.4.1 # via black pyupgrade==3.21.2 # via shed pyyaml==6.0.3 # via # libcst # sphinx-jsonschema readme-renderer==44.0 # via twine requests==2.32.5 # via # -r requirements/tools.in # requests-toolbelt # sphinx # sphinx-jsonschema # twine requests-toolbelt==1.0.0 # via twine restructuredtext-lint==2.0.2 # via -r requirements/tools.in rfc3986==2.0.0 # via twine rich==14.3.3 # via # pelican # twine roman-numerals==4.1.0 # via sphinx ruff==0.15.6 # via -r requirements/tools.in secretstorage==3.5.0 # via keyring shed==2024.1.1 # via -r requirements/tools.in six==1.17.0 # via python-dateutil snowballstemmer==3.0.1 # via sphinx sortedcontainers==2.4.0 # via # hypothesis (hypothesis-python/pyproject.toml) # sortedcontainers-stubs sortedcontainers-stubs==2.4.3 # via -r requirements/tools.in soupsieve==2.8.3 # via beautifulsoup4 sphinx==9.1.0 # via # -r requirements/tools.in # furo # sphinx-autobuild # sphinx-basic-ng # sphinx-codeautolink sphinx-autobuild==2025.8.25 # via -r requirements/tools.in sphinx-basic-ng==1.0.0b2 # via furo sphinx-codeautolink==0.17.5 # via -r requirements/tools.in sphinx-jsonschema==1.19.2 # via -r requirements/tools.in sphinx-selective-exclude==1.0.3 # via -r requirements/tools.in sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 # via sphinx sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlparse==0.5.5 # via django stack-data==0.6.3 # via ipython starlette==0.52.1 # via sphinx-autobuild tokenize-rt==6.2.0 # via pyupgrade tomli==2.4.0 # via -r requirements/tools.in tomli-w==1.2.0 # via tox tox==4.49.1 # via -r requirements/tools.in traitlets==5.14.3 # via # ipython # matplotlib-inline twine==6.2.0 # via -r requirements/tools.in types-cffi==1.17.0.20260307 # via types-pyopenssl types-click==7.1.8 # via -r requirements/tools.in types-pyopenssl==24.1.0.20240722 # via types-redis types-pytz==2026.1.1.20260304 # via -r requirements/tools.in types-redis==4.6.0.20241004 # via -r requirements/tools.in types-setuptools==82.0.0.20260210 # via types-cffi typing-extensions==4.15.0 # via # -r requirements/tools.in # beautifulsoup4 # mypy # pyright # sortedcontainers-stubs unidecode==1.4.0 # via pelican urllib3==2.6.3 # via # id # requests # twine uvicorn==0.41.0 # via sphinx-autobuild virtualenv==21.2.0 # via tox watchdog==6.0.0 # via -r requirements/tools.in watchfiles==1.1.1 # via # pelican # sphinx-autobuild wcwidth==0.6.0 # via prompt-toolkit websockets==16.0 # via sphinx-autobuild wheel==0.46.3 # via pip-tools # The following packages are considered to be unsafe in a requirements file: pip==26.0.1 # via pip-tools setuptools==82.0.1 # via pip-tools ================================================ FILE: tooling/README.md ================================================ # Hypothesis Build Tooling This is a piece of software for managing Hypothesis's build tasks, releases, etc. It's very Hypothesis specific, though it may become less so in the future. ================================================ FILE: tooling/codespell-dict.txt ================================================ sipport->support stratgy->strategy ================================================ FILE: tooling/codespell-ignore.txt ================================================ aas assertIn Copin crate damon nd ned nin strat tread rouge tey tham datas ================================================ FILE: tooling/scripts/common.sh ================================================ #!/usr/bin/env bash # This file is not really a script but is intended for sourcing from other # scripts so that they can share a common set of paths conveniently. set -o errexit set -o nounset HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT="$(git -C "$HERE" rev-parse --show-toplevel)" export ROOT export BUILD_RUNTIMES=${BUILD_RUNTIMES:-$HOME/.cache/hypothesis-build-runtimes} export BASE="$BUILD_RUNTIMES" export PYENV="$BASE/pyenv" export SNAKEPIT="$BASE/python-versions/" export XDG_CACHE_HOME="$BUILD_RUNTIMES/.cache" # Note: Deliberately ignoring BUILD_RUNTIMES configuration because we don't # want this to go in cache, because it takes up a huge amount of space and # slows everything down! export VIRTUALENVS="${TMPDIR:-/tmp}/.hypothesis-runtimes/virtualenvs/" pythonloc() { VERSION="$1" echo "$SNAKEPIT/$VERSION" } ================================================ FILE: tooling/scripts/ensure-python.sh ================================================ #!/usr/bin/env bash if [ -n "${CI:-}" ] ; then echo "::group::Ensure Python" ; fi set -o errexit set -o nounset set -x HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # shellcheck source=tooling/scripts/common.sh source "$HERE/common.sh" # This is to guard against multiple builds in parallel. The various installers will tend # to stomp all over each other if you do this and they haven't previously successfully # succeeded. We use a lock file to block progress so only one install runs at a time. # This script should be pretty fast once files are cached, so the loss of concurrency # is not a major problem. # This should be using the lockfile command, but that's not available on the # containerized travis and we can't install it without sudo. # It is unclear if this is actually useful. I was seeing behaviour that suggested # concurrent runs of the installer, but I can't seem to find any evidence of this lock # ever not being acquired. VERSION="$1" TARGET=$(pythonloc "$VERSION") if [ ! -e "$TARGET/bin/python" ] ; then mkdir -p "$BASE" LOCKFILE="$BASE/.install-lockfile" while true; do if mkdir "$LOCKFILE" 2>/dev/null; then echo "Successfully acquired installer." break else echo "Failed to acquire lock. Is another installer running? Waiting a bit." fi sleep $(( ( RANDOM % 10 ) + 1 )).$(( RANDOM % 100 ))s if (( $(date '+%s') > 300 + $(stat --format=%X "$LOCKFILE") )); then echo "We've waited long enough" rm -rf "$LOCKFILE" fi done trap 'rm -rf $LOCKFILE' EXIT if [ ! -d "$PYENV/.git" ]; then rm -rf "$PYENV" git clone https://github.com/pyenv/pyenv.git "$PYENV" else back=$PWD cd "$PYENV" git fetch || echo "Update failed to complete. Ignoring" git reset --hard origin/master cd "$back" fi # See if installing all of these will fix our build issues... if (command -v apt-get >/dev/null 2>&1) && { [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] ; }; then sudo apt-get update sudo apt-get install -y \ build-essential \ libbz2-dev \ libffi-dev \ libgdbm-dev \ libgdbm-compat-dev \ liblzma-dev \ libncurses5-dev \ libreadline-dev \ libsqlite3-dev \ libssl-dev \ tk-dev \ uuid-dev \ zlib1g-dev fi for _ in $(seq 5); do if OUTPUT=$("$BASE/pyenv/plugins/python-build/bin/python-build" "$VERSION" "$TARGET" 2>&1); then exit 0 fi if echo "$OUTPUT" | grep -q "definition not found"; then echo "Python version $VERSION is no longer available." echo "Please run 'make upgrade-requirements' to update to the latest version." exit 1 fi echo "Command failed. For a possible solution, visit" echo "https://github.com/pyenv/pyenv/wiki#suggested-build-environment." echo "Retrying..." sleep $(( ( RANDOM % 10 ) + 1 )).$(( RANDOM % 100 ))s done fi if [ -n "${CI:-}" ] ; then echo "::endgroup::" ; fi ================================================ FILE: tooling/scripts/tool-hash.py ================================================ #!/usr/bin/env python # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import hashlib import sys if __name__ == "__main__": print(hashlib.sha384(sys.stdin.read().encode()).hexdigest()[:10]) ================================================ FILE: tooling/setup.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import setuptools def local_file(name): return os.path.relpath(os.path.join(os.path.dirname(__file__), name)) SOURCE = local_file("src") README = local_file("README.md") setuptools.setup( name="hypothesis-tooling", # We don't actually ship this, it just has a setup.py for convenience. version="0.0.0", author="David R. MacIver", author_email="david@drmaciver.com", packages=setuptools.find_packages(SOURCE), package_dir={"": SOURCE}, url="https://github.com/HypothesisWorks/hypothesis-python/tree/master/tooling", license="MPL v2", description="A library for property-based testing", python_requires=">=3.7", long_description=open(README).read(), # noqa ) ================================================ FILE: tooling/src/hypothesistooling/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import shlex import subprocess from pathlib import Path def current_branch(): return ( subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]) .decode("ascii") .strip() ) def tags(): result = [ t.decode("ascii") for t in subprocess.check_output(["git", "tag"]).split(b"\n") ] assert len(set(result)) == len(result) return set(result) ROOT = Path( subprocess.check_output( ["git", "-C", os.path.dirname(__file__), "rev-parse", "--show-toplevel"] ) .decode("ascii") .strip() ) REPO_TESTS = ROOT / "whole_repo_tests" def hash_for_name(name): return subprocess.check_output(["git", "rev-parse", name]).decode("ascii").strip() def is_ancestor(a, b): check = subprocess.call(["git", "merge-base", "--is-ancestor", a, b]) assert 0 <= check <= 1 return check == 0 def merge_base(a, b): return subprocess.check_output(["git", "merge-base", a, b]).strip() def point_of_divergence(): return merge_base("HEAD", "origin/master") def has_changes(files): command = [ "git", "diff", "--no-patch", "--exit-code", point_of_divergence(), "HEAD", "--", *files, ] return subprocess.call(command) != 0 def has_uncommitted_changes(filename): return subprocess.call(["git", "diff", "--exit-code", filename]) != 0 def last_committer(): out, _ = subprocess.Popen( ["git", "log", "-1", "--pretty=format:%an"], stdout=subprocess.PIPE, universal_newlines=True, ).communicate() return out def git(*args): subprocess.check_call(("git", *args)) TOOLING_COMMITTER_NAME = "CI on behalf of the Hypothesis team" def configure_git(): git("config", "user.name", TOOLING_COMMITTER_NAME) git("config", "user.email", "david@drmaciver.com") def create_tag(tagname): assert tagname not in tags() git("tag", tagname) def push_tag(tagname): assert_can_release() subprocess.check_call(["git", "push", "origin", shlex.quote(tagname)]) subprocess.check_call(["git", "push", "origin", "HEAD:master"]) def assert_can_release(): assert not IS_PULL_REQUEST, "Cannot release from pull requests" def modified_files(): files = set() for command in [ [ "git", "diff", "--name-only", "--diff-filter=d", point_of_divergence(), "HEAD", ], ["git", "diff", "--name-only"], ]: diff_output = subprocess.check_output(command).decode("ascii") for l in diff_output.split("\n"): filepath = l.strip() if filepath and os.path.exists(filepath): files.add(Path(filepath)) return files def all_files() -> list[Path]: return [ Path(f) for f in subprocess.check_output(["git", "ls-files"]) .decode("ascii") .splitlines() if os.path.exists(f) ] def changed_files_from_master(): """Returns a list of files which have changed between a branch and master.""" files = set() command = ["git", "diff", "--name-only", "HEAD", "master"] diff_output = subprocess.check_output(command).decode("ascii") for line in diff_output.splitlines(): filepath = line.strip() if filepath: files.add(filepath) return files IS_PULL_REQUEST = os.environ.get("GITHUB_REF", "").startswith("refs/pull/") def all_projects(): import hypothesistooling.projects.hypothesispython as hp return [hp] ================================================ FILE: tooling/src/hypothesistooling/__main__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import pathlib import re import subprocess import sys from datetime import date from pathlib import Path from textwrap import indent import requests from coverage.config import CoverageConfig import hypothesistooling as tools import hypothesistooling.projects.hypothesispython as hp from hypothesistooling import installers as install, releasemanagement as rm from hypothesistooling.scripts import pip_tool TASKS = {} BUILD_FILES = tuple( os.path.join(tools.ROOT, f) for f in ["tooling", "requirements", ".github", "hypothesis-python/tox.ini"] ) TODAY = date.today().isoformat() def task(if_changed=()): if not isinstance(if_changed, tuple): if_changed = (if_changed,) def accept(fn): def wrapped(*args, **kwargs): if if_changed and tools.IS_PULL_REQUEST: if not tools.has_changes(if_changed + BUILD_FILES): changed = ", ".join(map(str, if_changed)) print(f"Skipping task due to no changes in {changed}") return fn(*args, **kwargs) wrapped.__name__ = fn.__name__ name = fn.__name__.replace("_", "-") if name != "": TASKS[name] = wrapped return wrapped return accept @task() def check_installed(): """No-op task that can be used to test for a successful install (so we don't fail to run if a previous install failed midway).""" def codespell(*files): pip_tool( "codespell", "--check-hidden", "--check-filenames", "--ignore-words=./tooling/codespell-ignore.txt", # passing a custom --dictionary disables the default dictionary by default. # Add it back in with --dictionary=-. "--dictionary=-", "--dictionary=./tooling/codespell-dict.txt", "--skip=__pycache__,.mypy_cache,.venv,.git,tlds-alpha-by-domain.txt", *files, ) @task() def lint(): pip_tool("ruff", "check", ".") codespell(*(p for p in tools.all_files() if not p.name.endswith("by-domain.txt"))) failed = False matches = subprocess.run( r"git grep -En '@(dataclasses\.)?dataclass\(.*\)' " "| grep -Ev 'frozen=.*slots=|slots=.*frozen='", shell=True, capture_output=True, text=True, ).stdout if matches: print("\nAll dataclass decorators must pass slots= and frozen= arguments:") print(indent(matches, " ")) failed = True matches = subprocess.run( r"git grep -nP '\b(the|as|a|to|because|user|test|about|from|only)\s+\1\b'", shell=True, capture_output=True, text=True, ).stdout if matches: print("\nFound duplicate words:") print(indent(matches, " ")) failed = True if failed: sys.exit(1) def do_release(package): if not package.has_release(): print(f"No release for {package.__name__}") return os.chdir(package.BASE_DIR) print("Updating changelog and version") package.update_changelog_and_version() print("Committing changes") rm.commit_pending_release(package) print("Building distribution") package.build_distribution() print("Looks good to release!") tag_name = package.tag_name() print(f"Creating tag {tag_name}") tools.create_tag(tag_name) tools.push_tag(tag_name) print("Uploading distribution") package.upload_distribution() @task() def deploy(): HEAD = tools.hash_for_name("HEAD") MASTER = tools.hash_for_name("origin/master") print("Current head: ", HEAD) print("Current master:", MASTER) if not tools.is_ancestor(HEAD, MASTER): print("Not deploying due to not being on master") sys.exit(0) if "TWINE_PASSWORD" not in os.environ: print("Running without access to secure variables, so no deployment") sys.exit(0) tools.configure_git() for project in tools.all_projects(): do_release(project) sys.exit(0) # See https://www.linuxfoundation.org/blog/copyright-notices-in-open-source-software-projects/ HEADER = """ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """.strip() # this pattern is copied from shed # https://github.com/Zac-HD/shed/blob/6471da71c5b5cc443443ef5ed072799db275e7c0/src/shed/__init__.py#L297 rst_pattern = re.compile( r"(?P" r"^(?P *)\.\. " r"(?Pjupyter-execute::|" r"invisible-code-block: python|" # magic rst comment for Sybil doctests r"(code|code-block|sourcecode|ipython):: (python|py|sage|python3|py3|numpy))\n" r"((?P=indent) +:.*\n)*" r"\n*" r")" r"(?P(^((?P=indent) +.*)?\n)+)", flags=re.MULTILINE, ) def remove_consecutive_newlines_in_rst(path: Path): # replace 2+ empty lines in `.. code-block:: python` blocks with just one empty # line content = path.read_text() processed_content = rst_pattern.sub( lambda m: m["before"] + re.sub(r"\n{3,}", "\n\n", m["code"]), content ) if processed_content != content: path.write_text(processed_content) @task() def format(): changed = tools.modified_files() format_all = os.environ.get("FORMAT_ALL", "").lower() == "true" if "requirements/tools.txt" in changed: # We've changed the tools, which includes a lot of our formatting # logic, so we need to rerun formatters. format_all = True paths = tools.all_files() if format_all else changed doc_paths_to_format = [p for p in sorted(paths) if p.suffix in {".rst", ".md"}] paths_to_format = [p for p in sorted(paths) if p.suffix == ".py"] if not (paths_to_format or doc_paths_to_format): return # .coveragerc lists several regex patterns to treat as nocover pragmas, and # we want to find (and delete) cases where # pragma: no cover is redundant. def warn(msg): raise Exception(msg) config = CoverageConfig() config.from_file(os.path.join(hp.BASE_DIR, ".coveragerc"), warn=warn, our_file=True) pattern = "|".join(l for l in config.exclude_list if "pragma" not in l) unused_pragma_pattern = re.compile(f"(({pattern}).*) # pragma: no (branch|cover)") last_header_line = HEADER.splitlines()[-1].rstrip() for p in paths_to_format: lines = [] with open(p, encoding="utf-8") as fp: shebang = None first = True in_header = True for l in fp: if first: first = False if l[:2] == "#!": shebang = l continue elif in_header and l.rstrip() == last_header_line: in_header = False lines = [] else: lines.append(unused_pragma_pattern.sub(r"\1", l)) source = "".join(lines).strip() with open(p, "w", encoding="utf-8") as fp: if shebang is not None: fp.write(shebang) fp.write("\n") fp.write(HEADER) if source: fp.write("\n\n") fp.write(source) fp.write("\n") codespell("--write-changes", *paths_to_format, *doc_paths_to_format) pip_tool("ruff", "check", "--fix-only", ".") pip_tool("shed", "--py310-plus", *paths_to_format, *doc_paths_to_format) for p in doc_paths_to_format: remove_consecutive_newlines_in_rst(p) VALID_STARTS = (HEADER.split()[0], "#!/usr/bin/env python") @task() def check_format(): format() n = max(map(len, VALID_STARTS)) bad = False for p in tools.all_files(): if p.suffix != ".py": continue with open(p, encoding="utf-8") as fp: start = fp.read(n) if not any(start.startswith(s) for s in VALID_STARTS): print(f"{p} has incorrect start {start!r}", file=sys.stderr) bad = True assert not bad try: check_not_changed() except Exception: box_width = 50 inner_width = box_width - 2 content_width = inner_width - 2 msg1 = "Note: code differed after formatting." msg2 = "To fix this, run:" msg3 = " ./build.sh format" lines = [ "", " " + "*" * box_width, " *" + " " * inner_width + "*", " * " + msg1 + " " * (content_width - len(msg1)) + "*", " *" + " " * inner_width + "*", " * " + msg2 + " " * (content_width - len(msg2)) + "*", " *" + " " * inner_width + "*", " * " + msg3 + " " * (content_width - len(msg3)) + "*", " *" + " " * inner_width + "*", " " + "*" * box_width, "", ] print("\n".join(lines), file=sys.stderr) raise def check_not_changed(): subprocess.check_call(["git", "diff", "--exit-code"]) @task() def compile_requirements(*, upgrade=False): if upgrade: extra = ["--upgrade", "--rebuild"] else: extra = [] for f in Path("requirements").glob("*.in"): out_file = f.with_suffix(".txt") pip_tool( "pip-compile", "--allow-unsafe", # future default, not actually unsafe "--resolver=backtracking", # new pip resolver, default in pip-compile 7+ *extra, str(f), "hypothesis-python/pyproject.toml", "--output-file", str(out_file), cwd=tools.ROOT, env={ "CUSTOM_COMPILE_COMMAND": "./build.sh upgrade-requirements", **os.environ, }, ) # Check that we haven't added anything to output files without adding to inputs out_pkgs = out_file.read_text(encoding="utf-8") for p in f.read_text(encoding="utf-8").splitlines(): p = p.lower().replace("_", "-") if re.fullmatch(r"[a-z-]+", p): assert p + "==" in out_pkgs, f"Package `{p}` deleted from {out_file}" out_file.write_text(out_pkgs.replace(f"{tools.ROOT}/", "")) def update_python_versions(): install.ensure_python(PYTHONS[ci_version]) where = os.path.expanduser("~/.cache/hypothesis-build-runtimes/pyenv/") subprocess.run( "git fetch && git reset --hard origin/master", cwd=where, shell=True, capture_output=True, ) cmd = "bin/pyenv install --list" result = subprocess.run( cmd, shell=True, stdout=subprocess.PIPE, cwd=where ).stdout.decode() # pyenv reports available versions in chronological order, so we keep the newest # *unless* our current ends with a digit (is stable) and the candidate does not. # (plus some special cases for the `t` suffix for free-threading builds) stable = re.compile(r".*3\.\d+.\d+t?$") min_minor_version = re.search( r'requires-python = ">= ?3.(\d+)"', Path("hypothesis-python/pyproject.toml").read_text(encoding="utf-8"), ).group(1) best = {} for line in map(str.strip, result.splitlines()): if m := re.match(r"(?:pypy)?3\.(?:9|\d\dt?)", line): key = m.group() curr = best.get(key, line) if ( (stable.match(line) or not stable.match(curr)) and not (line.endswith("-dev") and not curr.endswith("-dev")) and int(key.split(".")[-1].rstrip("t")) >= int(min_minor_version) and key.endswith("t") == line.endswith(("t", "t-dev")) ): best[key] = line if best == PYTHONS: return # Write the new mapping back to this file thisfile = pathlib.Path(__file__) before = thisfile.read_text(encoding="utf-8") after = re.sub(r"\nPYTHONS = \{[^{}]+\}", f"\nPYTHONS = {best}", before) thisfile.write_text(after, encoding="utf-8") pip_tool("shed", str(thisfile)) # Automatically sync ci_version with the version in build.sh build_sh = tools.ROOT / "build.sh" sh_before = build_sh.read_text(encoding="utf-8") sh_after = re.sub(r"3\.\d\d?\.\d\d?", best[ci_version], sh_before) if sh_before != sh_after: build_sh.unlink() # so bash doesn't reload a modified file build_sh.write_text(sh_after, encoding="utf-8") build_sh.chmod(0o755) DJANGO_VERSIONS = { "4.2": "4.2.29", "5.2": "5.2.12", "6.0": "6.0.3", } def update_django_versions(): # https://endoflife.date/django makes it easier to track these releases = requests.get("https://endoflife.date/api/django.json").json() versions = {r["cycle"]: r["latest"] for r in releases[::-1] if TODAY <= r["eol"]} if versions == DJANGO_VERSIONS: return # Write the new mapping back to this file thisfile = pathlib.Path(__file__) before = thisfile.read_text(encoding="utf-8") after = re.sub( r"DJANGO_VERSIONS = \{[^{}]+\}", "DJANGO_VERSIONS = " + repr(versions).replace("}", ",}"), before, ) thisfile.write_text(after, encoding="utf-8") pip_tool("shed", str(thisfile)) # Update the minimum version in pyproject.toml pyproject_toml = hp.BASE_DIR / "pyproject.toml" content = re.sub( r"django>=\d+\.\d+", f"django>={min(versions, key=float)}", pyproject_toml.read_text(encoding="utf-8"), ) pyproject_toml.write_text(content, encoding="utf-8") # Automatically sync ci_version with the version in build.sh tox_ini = hp.BASE_DIR / "tox.ini" content = tox_ini.read_text(encoding="utf-8") print(versions) for short, full in versions.items(): content = re.sub( rf"django=={short}(\.\d+)?", rf"django=={full}", content, ) tox_ini.write_text(content, encoding="utf-8") def update_pyodide_versions(): def version_tuple(v: str) -> tuple[int, int, int]: return tuple(int(x) for x in v.split(".")) # type: ignore vers_re = r"(\d+\.\d+\.\d+)" all_pyodide_build_versions = re.findall( f"pyodide_build-{vers_re}-py3-none-any.whl", # excludes pre-releases requests.get("https://pypi.org/simple/pyodide-build/").text, ) pyodide_build_version = max( # Don't just pick the most recent version; find the highest stable version. set(all_pyodide_build_versions), key=version_tuple, ) cross_build_environments_url = "https://raw.githubusercontent.com/pyodide/pyodide/refs/heads/main/pyodide-cross-build-environments.json" cross_build_environments_data = requests.get(cross_build_environments_url).json() # Find the latest stable release for the Pyodide runtime/xbuildenv that is compatible # with the pyodide-build version we found stable_releases = [ release for release in cross_build_environments_data["releases"].values() if re.fullmatch(vers_re, release["version"]) ] compatible_releases = [] for release in stable_releases: # sufficiently large values min_build_version = release.get("min_pyodide_build_version", "0.0.0") max_build_version = release.get("max_pyodide_build_version", "999.999.999") # Perform version comparisons to avoid getting an incompatible pyodide-build version # with the Pyodide runtime if ( version_tuple(min_build_version) <= version_tuple(pyodide_build_version) <= version_tuple(max_build_version) ): compatible_releases.append(release) if not compatible_releases: raise RuntimeError( f"No compatible Pyodide release found for pyodide-build {pyodide_build_version}" ) pyodide_release = max( compatible_releases, key=lambda release: version_tuple(release["version"]), ) pyodide_version = pyodide_release["version"] python_version = pyodide_release["python_version"] ci_file = tools.ROOT / ".github/workflows/main.yml" config = ci_file.read_text(encoding="utf-8") for name, var in [ ("PYODIDE", pyodide_version), ("PYODIDE_BUILD", pyodide_build_version), ("PYTHON", python_version), ]: config = re.sub(f"{name}_VERSION: {vers_re}", f"{name}_VERSION: {var}", config) ci_file.write_text(config, encoding="utf-8") def update_vendored_files(): vendor = pathlib.Path(hp.PYTHON_SRC) / "hypothesis" / "vendor" # Turns out that as well as adding new gTLDs, IANA can *terminate* old ones url = "http://data.iana.org/TLD/tlds-alpha-by-domain.txt" fname = vendor / url.split("/")[-1] new = requests.get(url).content # If only the timestamp in the header comment has changed, skip the update. if fname.read_bytes().splitlines()[1:] != new.splitlines()[1:]: fname.write_bytes(new) # Always require the most recent version of tzdata - we don't need to worry about # pre-releases because tzdata is a 'latest data' package (unlike pyodide-build). # Our crosshair extra is research-grade, so we require latest versions there too. pyproject_toml = pathlib.Path(hp.BASE_DIR, "pyproject.toml") new = pyproject_toml.read_text(encoding="utf-8") for pkgname in ("tzdata", "crosshair-tool", "hypothesis-crosshair"): pkg_url = f"https://pypi.org/pypi/{pkgname}/json" pkg_version = requests.get(pkg_url).json()["info"]["version"] new = re.sub(rf"{pkgname}>=([a-z0-9.]+)", f"{pkgname}>={pkg_version}", new) pyproject_toml.write_text(new, encoding="utf-8") def has_diff(file_or_directory): diff = ["git", "diff", "--no-patch", "--exit-code", "--", file_or_directory] return subprocess.call(diff) != 0 @task() def upgrade_requirements(): update_vendored_files() compile_requirements(upgrade=True) subprocess.call(["./build.sh", "format"], cwd=tools.ROOT) # exits 1 if changed if has_diff(hp.PYTHON_SRC) and not os.path.isfile(hp.RELEASE_FILE): msg = hp.get_autoupdate_message(domainlist_changed=has_diff(hp.DOMAINS_LIST)) with open(hp.RELEASE_FILE, mode="w", encoding="utf-8") as f: f.write(f"RELEASE_TYPE: patch\n\n{msg}") update_python_versions() update_pyodide_versions() update_django_versions() subprocess.call(["git", "add", "."], cwd=tools.ROOT) @task() def check_requirements(): compile_requirements(upgrade=False) @task(if_changed=hp.HYPOTHESIS_PYTHON) def documentation(): try: if hp.has_release(): hp.update_changelog_and_version() hp.build_docs() finally: subprocess.check_call( ["git", "checkout", "docs/changelog.rst", "src/hypothesis/version.py"], cwd=hp.HYPOTHESIS_PYTHON, ) @task() def website(): subprocess.call([sys.executable, "-m", "pelican"], cwd=tools.ROOT / "website") @task() def live_website(): subprocess.call( [sys.executable, "-m", "pelican", "--autoreload", "--listen"], cwd=tools.ROOT / "website", ) @task() def live_docs(): pip_tool( "sphinx-autobuild", "docs", "docs/_build/html", "--watch", "src", "--open-browser", cwd=hp.HYPOTHESIS_PYTHON, ) def run_tox(task, version, *args): python = install.python_executable(version) # Create a version of the name that tox will pick up for the correct # interpreter alias. linked_version = os.path.basename(python) + ALIASES[version] try: os.symlink(python, linked_version) except FileExistsError: pass env = dict(os.environ) python = install.python_executable(version) env["PATH"] = os.path.dirname(python) + ":" + env["PATH"] # Set environment variable for tox to use in basepython substitution if version.startswith("pypy"): # For PyPy, use the version name from e.g. "pypy3.11-7.3.20" # to match tox's environment name inference. env["TOX_PYTHON_VERSION"] = version.split("-")[0] # "pypy3.11" else: env["TOX_PYTHON_VERSION"] = ALIASES[version] # "python3.12" print(env["PATH"]) pip_tool("tox", "-e", task, *args, env=env, cwd=hp.HYPOTHESIS_PYTHON) # update_python_versions(), above, keeps the contents of this dict up to date. # When a version is added or removed, manually update the env lists in tox.ini and # workflows/main.yml, and the `Programming Language ::` specifiers in pyproject.toml PYTHONS = { "3.10": "3.10.20", "3.11": "3.11.15", "3.12": "3.12.13", "3.13": "3.13.12", "3.13t": "3.13t-dev", "3.14": "3.14.3", "3.14t": "3.14t-dev", "3.15": "3.15.0a7", "3.15t": "3.15t-dev", "pypy3.10": "pypy3.10-7.3.19", "pypy3.11": "pypy3.11-7.3.20", } ci_version = "3.14" # Keep this in sync with GH Actions main.yml and .readthedocs.yml python_tests = task( if_changed=( hp.PYTHON_SRC, hp.PYTHON_TESTS, hp.HYPOTHESIS_PYTHON / "pyproject.toml", tools.ROOT / "tooling", hp.HYPOTHESIS_PYTHON / "scripts", ) ) # ALIASES are the executable names for each Python version ALIASES = {} for key, version in PYTHONS.items(): if key.startswith("pypy"): ALIASES[version] = "pypy3" name = key.replace(".", "") else: ALIASES[version] = f"python{key}" name = f"py3{key[2:]}" TASKS[f"check-{name}"] = python_tests( lambda n=f"{name}-full", v=version, *args: run_tox(n, v, *args) ) for subtask in ( "brief", "full", "cover", "rest", "nocover", "niche", "custom", ): TASKS[f"check-{name}-{subtask}"] = python_tests( lambda n=f"{name}-{subtask}", v=version, *args: run_tox(n, v, *args) ) @python_tests def check_py310_pyjion(*args): run_tox("py310-pyjion", PYTHONS["3.10"], *args) @task() def tox(*args): if len(args) < 2: print("Usage: ./build.sh tox TOX_ENV PY_VERSION [tox args]") sys.exit(1) run_tox(*args) def standard_tox_task(name, py=ci_version): TASKS["check-" + name] = python_tests( lambda *args: run_tox(name, PYTHONS.get(py, py), *args) ) standard_tox_task("py311-pytest62", py="3.11") # hits "ast.Str is deprecated" in 3.12+ standard_tox_task("pytest74") standard_tox_task("pytest84") standard_tox_task("pytest9") dj_version = max(ci_version, "3.12") for n in DJANGO_VERSIONS: standard_tox_task(f"django{n.replace('.', '')}", py=dj_version) # we also test no-contrib on the latest django version standard_tox_task("django-nocontrib", py=dj_version) # test each pandas version with the latest python version they support standard_tox_task("py310-pandas11", py="3.10") standard_tox_task("py310-pandas12", py="3.10") standard_tox_task("py310-pandas13", py="3.10") standard_tox_task("py310-pandas14", py="3.10") standard_tox_task("py311-pandas15", py="3.11") standard_tox_task("py311-pandas20", py="3.11") standard_tox_task("py312-pandas21", py="3.12") standard_tox_task("py313-pandas22", py="3.13") for kind in ("cover", "nocover", "niche", "custom"): standard_tox_task(f"crosshair-{kind}") for kind in ("rest", "nocover"): # Note, in CI these are executed on alternative platforms (e.g., windows) # directly in tox (and not via build.sh) standard_tox_task(f"alt-{kind}") standard_tox_task("threading") standard_tox_task("py310-oldestnumpy", py="3.10") standard_tox_task("numpy-nightly", py="3.12") standard_tox_task("coverage") standard_tox_task("conjecture-coverage") @task() def check_quality(*args): run_tox("quality", PYTHONS[ci_version], *args) @task(if_changed=(hp.PYTHON_SRC, os.path.join(hp.HYPOTHESIS_PYTHON, "examples"))) def check_examples3(*args): run_tox("examples3", PYTHONS[ci_version], *args) @task() def check_whole_repo_tests(*args): install.ensure_shellcheck() subprocess.check_call( [sys.executable, "-m", "pip", "install", "--upgrade", hp.HYPOTHESIS_PYTHON] ) if not args: args = ["-n", "auto", tools.REPO_TESTS / "whole_repo"] subprocess.check_call([sys.executable, "-m", "pytest", *args]) @task() def check_documentation(*args): install.ensure_shellcheck() subprocess.check_call( [sys.executable, "-m", "pip", "install", "--upgrade", hp.HYPOTHESIS_PYTHON] ) if not args: args = ["-n", "auto", tools.REPO_TESTS / "documentation"] subprocess.check_call([sys.executable, "-m", "pytest", *args]) @task() def check_types(*args): install.ensure_shellcheck() subprocess.check_call( [sys.executable, "-m", "pip", "install", "--upgrade", hp.HYPOTHESIS_PYTHON] ) if not args: args = ["-n", "auto", tools.REPO_TESTS / "types"] subprocess.check_call([sys.executable, "-m", "pytest", *args]) @task() def check_types_api(*args): install.ensure_shellcheck() subprocess.check_call( [sys.executable, "-m", "pip", "install", "--upgrade", hp.HYPOTHESIS_PYTHON] ) if not args: ignore = ["--ignore", tools.REPO_TESTS / "types/test_hypothesis.py"] args = ["-n", "auto", tools.REPO_TESTS / "types"] + ignore subprocess.check_call([sys.executable, "-m", "pytest", *args]) @task() def check_types_hypothesis(*args): install.ensure_shellcheck() subprocess.check_call( [sys.executable, "-m", "pip", "install", "--upgrade", hp.HYPOTHESIS_PYTHON] ) if not args: testcase = "types/test_hypothesis.py" args = ["-n", "auto", tools.REPO_TESTS / testcase] subprocess.check_call([sys.executable, "-m", "pytest", *args]) @task() def shell(): import IPython IPython.start_ipython([]) @task() def python(*args): os.execv(sys.executable, (sys.executable, *args)) @task() def tasks(): """Print a list of all task names supported by the build system.""" for task_name in sorted(TASKS.keys()): print(" " + task_name) if __name__ == "__main__": if "SNAKEPIT" not in os.environ: print( "This module should not be executed directly, but instead via " "build.sh (which sets up its environment)" ) sys.exit(1) if len(sys.argv) > 1: task_to_run = sys.argv[1] args = sys.argv[2:] else: task_to_run = os.environ.get("TASK") args = () if task_to_run is None: print( "No task specified. Either pass the task to run as an " "argument or as an environment variable TASK. " '(Use "./build.sh tasks" to list all supported task names.)' ) sys.exit(1) if task_to_run not in TASKS: print(f"\nUnknown task {task_to_run!r}. Available tasks are:") tasks() sys.exit(1) try: TASKS[task_to_run](*args) except subprocess.CalledProcessError as e: sys.exit(e.returncode) ================================================ FILE: tooling/src/hypothesistooling/installers.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Module for obtaining various versions of Python. Currently this is a thin shim around pyenv, but it would be nice to have this work on Windows as well by using Anaconda (as our build already does). """ import os import shutil import subprocess from hypothesistooling import scripts from hypothesistooling.junkdrawer import once HOME = os.environ["HOME"] def __python_executable(version): return os.path.join(scripts.SNAKEPIT, version, "bin", "python") def python_executable(version): ensure_python(version) return __python_executable(version) PYTHONS = set() def ensure_python(version): if version in PYTHONS: return scripts.run_script("ensure-python.sh", version) target = __python_executable(version) assert os.path.exists(target), target PYTHONS.add(version) STACK = os.path.join(HOME, ".local", "bin", "stack") SHELLCHECK = shutil.which("shellcheck") or os.path.join( HOME, ".local", "bin", "shellcheck" ) def ensure_stack(): if os.path.exists(STACK): return subprocess.check_call("mkdir -p ~/.local/bin", shell=True) # if you're on macos, this will error with "--wildcards is not supported" # or similar. You should put shellcheck on your PATH with your package # manager of choice; eg `brew install shellcheck`. subprocess.check_call( "curl -L https://www.stackage.org/stack/linux-x86_64 " "| tar xz --wildcards --strip-components=1 -C $HOME" "/.local/bin '*/stack'", shell=True, ) @once def update_stack(): ensure_stack() subprocess.check_call([STACK, "update"]) @once def ensure_shellcheck(): if os.path.exists(SHELLCHECK): return update_stack() subprocess.check_call([STACK, "install", "ShellCheck"]) ================================================ FILE: tooling/src/hypothesistooling/junkdrawer.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Dumping ground module for things that don't have anywhere better to go. See https://twitter.com/betsythemuffin/status/1003313844108824584 """ import ast import os from contextlib import contextmanager @contextmanager def in_dir(d): prev = os.getcwd() try: os.chdir(d) yield finally: os.chdir(prev) def once(fn): def accept(): if accept.has_been_called: return fn() accept.has_been_called = True accept.has_been_called = False accept.__name__ = fn.__name__ return accept def unlink_if_present(path): try: os.unlink(path) except FileNotFoundError: pass def unquote_string(s): return ast.literal_eval(s) ================================================ FILE: tooling/src/hypothesistooling/projects/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: tooling/src/hypothesistooling/projects/hypothesispython.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import json import os import re import shutil import subprocess import sys import requests import tomli import hypothesistooling as tools from hypothesistooling import releasemanagement as rm PACKAGE_NAME = "hypothesis-python" HYPOTHESIS_PYTHON = tools.ROOT / PACKAGE_NAME PYTHON_TAG_PREFIX = "hypothesis-python-" BASE_DIR = HYPOTHESIS_PYTHON PYTHON_SRC = HYPOTHESIS_PYTHON / "src" PYTHON_TESTS = HYPOTHESIS_PYTHON / "tests" DOMAINS_LIST = PYTHON_SRC / "hypothesis" / "vendor" / "tlds-alpha-by-domain.txt" RELEASE_FILE = HYPOTHESIS_PYTHON / "RELEASE.rst" RELEASE_SAMPLE_FILE = HYPOTHESIS_PYTHON / "RELEASE-sample.rst" assert PYTHON_SRC.exists() __version__ = None __version_info__ = None VERSION_FILE = os.path.join(PYTHON_SRC, "hypothesis/version.py") with open(VERSION_FILE, encoding="utf-8") as fp: exec(fp.read()) assert __version__ is not None assert __version_info__ is not None def has_release(): return RELEASE_FILE.exists() def has_release_sample(): return RELEASE_SAMPLE_FILE.exists() def parse_release_file(): return rm.parse_release_file(RELEASE_FILE) def has_source_changes(): return tools.has_changes([PYTHON_SRC]) def build_docs(*, builder="html", only=(), to=None): # See https://www.sphinx-doc.org/en/stable/man/sphinx-build.html tools.scripts.pip_tool( "sphinx-build", "--fail-on-warning", "--show-traceback", "--fresh-env", "--builder", builder, "docs", "docs/_build/" + (builder if to is None else to), *only, cwd=HYPOTHESIS_PYTHON, ) CHANGELOG_ANCHOR = re.compile(r"^\.\. _v\d+\.\d+\.\d+:$", flags=re.MULTILINE) CHANGELOG_BORDER = re.compile(r"^-+$", flags=re.MULTILINE) CHANGELOG_HEADER = re.compile( r"^\d+\.\d+\.\d+ - \d\d\d\d-\d\d-\d\d$", flags=re.MULTILINE ) def update_changelog_and_version(): global __version_info__ global __version__ contents = changelog() assert "\r" not in contents lines = contents.split("\n") for i, l in enumerate(lines): if CHANGELOG_ANCHOR.match(l): assert CHANGELOG_BORDER.match(lines[i + 2]), repr(lines[i + 2]) assert CHANGELOG_HEADER.match(lines[i + 3]), repr(lines[i + 3]) assert CHANGELOG_BORDER.match(lines[i + 4]), repr(lines[i + 4]) assert lines[i + 3].startswith( __version__ ), f"{__version__=} {lines[i + 3]=}" beginning = "\n".join(lines[:i]) rest = "\n".join(lines[i:]) assert f"{beginning}\n{rest}" == contents break release_type, release_contents = parse_release_file() new_version_string, new_version_info = rm.bump_version_info( __version_info__, release_type ) __version_info__ = new_version_info __version__ = new_version_string if release_type == "major": major, _, _ = __version_info__ old = f"Hypothesis {major - 1}.x" beginning = beginning.replace(old, f"Hypothesis {major}.x") rest = "\n".join([old, len(old) * "=", "", rest]) rm.replace_assignment(VERSION_FILE, "__version_info__", repr(new_version_info)) heading_for_new_version = f"{new_version_string} - {rm.release_date_string()}" border_for_new_version = "-" * len(heading_for_new_version) new_changelog_parts = [ beginning.strip(), "", f".. _v{new_version_string}:", "", border_for_new_version, heading_for_new_version, border_for_new_version, "", release_contents, "", rest, ] CHANGELOG_FILE.write_text("\n".join(new_changelog_parts), encoding="utf-8") # Replace the `since="RELEASEDAY"` argument to `note_deprecation` # with today's date, to record it for future reference. before = 'since="RELEASEDAY"' after = before.replace("RELEASEDAY", rm.release_date_string()) for root, _, files in os.walk(PYTHON_SRC): for fname in (os.path.join(root, f) for f in files if f.endswith(".py")): with open(fname, encoding="utf-8") as f: contents = f.read() if before in contents: with open(fname, "w", encoding="utf-8") as f: f.write(contents.replace(before, after)) update_pyproject_toml() def update_pyproject_toml(): # manually write back these changes using regex instead of pulling in a # toml dependency for writing. tomli doesn't support writing, and # tomli-w doesn't support writing with comments. toml_p = HYPOTHESIS_PYTHON / "pyproject.toml" toml_data = tomli.loads(toml_p.read_text()) extras = toml_data["project"]["optional-dependencies"] extras.pop("all") readme = (tools.ROOT / "README.md").read_text() content = toml_p.read_text() content = re.sub( r'\[project\.readme\].*content-type = "text/markdown"', f'[project.readme]\ntext = """{readme}"""\ncontent-type = "text/markdown"', content, flags=re.DOTALL, ) all_extras = sorted(set(sum(extras.values(), []))) all_extras = json.dumps(all_extras).replace("\n", "\\n") content = re.sub( r"^all = \[.*\]$", f"all = {all_extras}", content, flags=re.MULTILINE, ) toml_p.write_text(content) CHANGELOG_FILE = HYPOTHESIS_PYTHON / "docs" / "changelog.rst" DIST = HYPOTHESIS_PYTHON / "dist" def changelog(): return CHANGELOG_FILE.read_text(encoding="utf-8") def build_distribution(): if os.path.exists(DIST): shutil.rmtree(DIST) subprocess.check_output([sys.executable, "-m", "build", "--outdir", DIST]) def upload_distribution(): tools.assert_can_release() subprocess.check_call( [ sys.executable, "-m", "twine", "upload", "--skip-existing", "--username=__token__", os.path.join(DIST, "*"), ] ) # Construct plain-text + markdown version of this changelog entry, # with link to canonical source. build_docs(builder="text", only=["docs/changelog.rst"]) textfile = os.path.join( HYPOTHESIS_PYTHON, "docs", "_build", "text", "changelog.txt" ) with open(textfile, encoding="utf-8") as f: lines = f.readlines() entries = [i for i, l in enumerate(lines) if CHANGELOG_HEADER.match(l)] anchor = current_version().replace(".", "-") changelog_body = ( "".join(lines[entries[0] + 2 : entries[1]]).strip() + "\n\n*[The canonical version of these notes (with links) is on readthedocs.]" f"(https://hypothesis.readthedocs.io/en/latest/changelog.html#v{anchor})*" ) # Create a GitHub release, to trigger Zenodo DOI minting. See # https://developer.github.com/v3/repos/releases/#create-a-release resp = requests.post( "https://api.github.com/repos/HypothesisWorks/hypothesis/releases", headers={ "Accept": "application/vnd.github+json", "Authorization": f"Bearer {os.environ['GH_TOKEN']}", "X-GitHub-Api-Version": "2022-11-28", }, json={ "tag_name": tag_name(), "name": "Hypothesis for Python - version " + current_version(), "body": changelog_body, }, timeout=120, # seconds ) # TODO: work out why this is 404'ing despite success (?!?) and fix it try: resp.raise_for_status() except Exception: import traceback traceback.print_exc() def current_version(): return __version__ def latest_version(): versions = [] for t in tools.tags(): if t.startswith(PYTHON_TAG_PREFIX): t = t.removeprefix(PYTHON_TAG_PREFIX) else: continue assert t == t.strip() parts = t.split(".") assert len(parts) == 3 v = tuple(map(int, parts)) versions.append((v, t)) _, latest = max(versions) return latest def tag_name(): return PYTHON_TAG_PREFIX + __version__ def get_autoupdate_message(domainlist_changed): if domainlist_changed: return ( "This patch updates our vendored `list of top-level domains " "`__,\nwhich is used by the " "provisional :func:`~hypothesis.provisional.domains` strategy.\n" ) return ( "This patch updates our autoformatting tools, " "improving our code style without any API changes." ) ================================================ FILE: tooling/src/hypothesistooling/releasemanagement.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. """Helpful common code for release management tasks that is shared across multiple projects. Note that most code in here is brittle and specific to our build and probably makes all sorts of undocumented assumptions, even as it looks like a nice tidy reusable set of functionality. """ import re from datetime import datetime, timezone import hypothesistooling as tools __RELEASE_DATE_STRING = None def release_date_string(): """Returns a date string that represents what should be considered "today" for the purposes of releasing, and ensure that we don't change part way through a release.""" global __RELEASE_DATE_STRING if __RELEASE_DATE_STRING is None: __RELEASE_DATE_STRING = datetime.now(timezone.utc).strftime("%Y-%m-%d") return __RELEASE_DATE_STRING def assignment_matcher(name): """ Matches a single line of the form (some space)name = (some value). e.g. " foo = 1". The whole line up to the assigned value is the first matching group, the rest of the line is the second matching group. i.e. group 1 is the assignment, group 2 is the value. In the above example group 1 would be " foo = " and group 2 would be "1" """ return re.compile(rf"\A(\s*{re.escape(name)}\s*=\s*)(.+)\Z") def extract_assignment_from_string(contents, name): lines = contents.split("\n") matcher = assignment_matcher(name) for l in lines: match = matcher.match(l) if match is not None: return match[2].strip() raise ValueError(f"Key {name} not found in {contents}") def extract_assignment(filename, name): with open(filename, encoding="utf-8") as i: return extract_assignment_from_string(i.read(), name) def replace_assignment_in_string(contents, name, value): lines = contents.split("\n") matcher = assignment_matcher(name) count = 0 for i, l in enumerate(lines): match = matcher.match(l) if match is not None: count += 1 lines[i] = match[1] + value if count == 0: raise ValueError(f"Key {name} not found in {contents}") if count > 1: raise ValueError(f"Key {name} found {count} times in {contents}") return "\n".join(lines) def replace_assignment(filename, name, value): """Replaces a single assignment of the form key = value in a file with a new value, attempting to preserve the existing format. This is fairly fragile - in particular it knows nothing about the file format. The existing value is simply the rest of the line after the last space after the equals. """ with open(filename, encoding="utf-8") as f: contents = f.read() result = replace_assignment_in_string(contents, name, value) with open(filename, "w", encoding="utf-8") as f: f.write(result) RELEASE_TYPE = re.compile(r"^RELEASE_TYPE: +(major|minor|patch)") MAJOR = "major" MINOR = "minor" PATCH = "patch" VALID_RELEASE_TYPES = (MAJOR, MINOR, PATCH) def parse_release_file(filename): with open(filename, encoding="utf-8") as i: return parse_release_file_contents(i.read(), filename) def parse_release_file_contents(release_contents, filename): release_lines = [l.rstrip() for l in release_contents.split("\n")] m = RELEASE_TYPE.match(release_lines[0]) if m is not None: release_type = m.group(1) if release_type not in VALID_RELEASE_TYPES: raise ValueError(f"Unrecognised release type {release_type!r}") del release_lines[0] release_contents = "\n".join(release_lines).strip() else: raise ValueError( f"{filename} does not start by specifying release type. The first " "line of the file should be RELEASE_TYPE: followed by one of " "major, minor, or patch, to specify the type of release that " "this is (i.e. which version number to increment). Instead the " f"first line was {release_lines[0]!r}" ) return release_type, release_contents def bump_version_info(version_info, release_type): new_version = list(version_info) bump = VALID_RELEASE_TYPES.index(release_type) new_version[bump] += 1 for i in range(bump + 1, len(new_version)): new_version[i] = 0 new_version = tuple(new_version) new_version_string = ".".join(map(str, new_version)) return new_version_string, new_version def update_markdown_changelog(changelog, name, version, entry): with open(changelog, encoding="utf-8") as f: prev_contents = f.read() title = f"# {name} {version} ({release_date_string()})\n\n" with open(changelog, "w", encoding="utf-8") as f: f.write(title) f.write(entry.strip()) f.write("\n\n") f.write(prev_contents) def parse_version(version): return tuple(map(int, version.split("."))) def commit_pending_release(project): """Create a commit with the new release.""" tools.git("rm", project.RELEASE_FILE) tools.git("add", "-u", project.BASE_DIR) tools.git( "commit", "-m", f"Bump {project.PACKAGE_NAME} version to {project.current_version()} " "and update changelog\n\n[skip ci]", ) ================================================ FILE: tooling/src/hypothesistooling/scripts.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import re import shlex import subprocess import sys from hypothesistooling import ROOT def print_command(command, args): args = list(args) ranges = [] for i, v in enumerate(args): if os.path.exists(v): if not ranges or ranges[-1][-1] < i - 1: ranges.append([i, i]) elif ranges[-1][-1] + 1 == i: ranges[-1][-1] += 1 for i, j in ranges: if j > i: args[i] = "..." for k in range(i + 1, j + 1): args[k] = None args = [v for v in args if v is not None] print(command, *map(shlex.quote, args)) def run_script(script, *args, **kwargs): print_command(script, args) return subprocess.check_call([os.path.join(SCRIPTS, script), *args], **kwargs) SCRIPTS = ROOT / "tooling" / "scripts" COMMON = SCRIPTS / "common.sh" def __calc_script_variables(): exports = re.compile(r"^export ([A-Z_]+)(=|$)", flags=re.MULTILINE) common = COMMON.read_text(encoding="utf-8") for name, _ in exports.findall(common): globals()[name] = os.environ[name] try: __calc_script_variables() except Exception: pass def tool_path(name): return os.path.join(os.path.dirname(sys.executable), name) def pip_tool(name, *args, **kwargs): args = [str(arg) for arg in args] print_command(name, args) r = subprocess.call([tool_path(name), *args], **kwargs) if r != 0: sys.exit(r) ================================================ FILE: website/archive-redirect.html ================================================ Redirecting to Articles

Redirecting to articles...

================================================ FILE: website/content/2016-04-15-economics-of-software-correctness.md ================================================ --- tags: writing-good-software, non-technical date: 2016-04-15 15:00 title: The Economics of Software Correctness author: drmaciver --- You have probably never written a significant piece of correct software. That's not a value judgement. It's certainly not a criticism of your competence. I can say with almost complete confidence that every non-trivial piece of software I have written contains at least one bug. You *might* have written small libraries that are essentially bug free, but the chance that you have written a non-trivial bug free program is tantamount to zero. I don't even mean this in some pedantic academic sense. I'm talking about behaviour where if someone spotted it and pointed it out to you you would probably admit that it's a bug. It might even be a bug that you cared about. Why is this? Well, lets start with why it's not: It's not because we don't know how to write correct software. We've known how to write software that is more or less correct (or at least vastly closer to correct than the norm) for a while now. If you look at the NASA development process they're pretty much doing it. Also, if you look at the NASA development process you will probably conclude that we can't do that. It's orders of magnitude more work than we ever put into software development. It's process heavy, laborious, and does not adapt well to changing requirements or tight deadlines. The problem is not that we don't know how to write correct software. The problem is that correct software is too expensive. And "too expensive" doesn't mean "It will knock 10% off our profit margins, we couldn't possibly do that". It means "if our software cost this much to make, nobody would be willing to pay a price we could afford to sell it at". It may also mean "If our software took this long to make then someone else will release a competing product two years earlier than us, everyone will use that, and when ours comes along nobody will be interested in using it". ("sell" and "release" here can mean a variety of things. It can mean that terribly unfashionable behaviour where people give you money and you give them a license to your software. It can mean subscriptions. It can mean ad space. It can even mean paid work. I'm just going to keep saying sell and release). NASA can do it because when they introduce a software bug they potentially lose some combination of billions of dollars, years of work and many lives. When that's the cost of a bug, spending that much time and money on correctness seems like a great deal. Safety critical industries like medical technology and aviation can do it for similar reasons ([buggy medical technology kills people](https://en.wikipedia.org/wiki/Therac-25) and [you don't want your engines power cycling themselves midflight](http://www.engadget.com/2015/05/01/boeing-787-dreamliner-software-bug/)). The rest of us aren't writing safety critical software, and as a result people aren't willing to pay for that level of correctness. So the result is that we write software with bugs in it, and we adopt a much cheaper software testing methodology: We ship it and see what happens. Inevitably some user will find a bug in our software. Probably many users will find many bugs in our software. And this means that we're turning our users into our QA department. Which, to be clear, is fine. Users have stated the price that they're willing to pay, and that price does not include correctness, so they're getting software that is not correct. I think we all feel bad about shipping buggy software, so let me emphasise this here: Buggy software is not a moral failing. The option to ship correct software is simply not on the table, so why on earth should we feel bad about not taking it? But in another sense, turning our users into a QA department is a terrible idea. Why? Because users are not actually good at QA. QA is a complicated professional skill which very few people can do well. Even skilled developers often don't know how to write a good bug report. How can we possibly expect our users to? The result is long and frustrating conversations with users in which you try to determine whether what they're seeing is actually a bug or a misunderstanding (although treating misunderstandings as bugs is a good idea too), trying to figure out what the actual bug is, etc. It's a time consuming process which ends up annoying the user and taking up a lot of expensive time from developers and customer support. And that's of course if the users tell you at all. Some users will just try your software, decide it doesn't work, and go away without ever saying anything to you. This is particularly bad for software where you can't easily tell who is using it. Also, some of our users are actually adversaries. They're not only not going to tell you about bugs they find, they're going to actively try to keep you from finding out because they're using it to steal money and/or data from you. So this is the problem with shipping buggy software: Bugs found by users are more expensive than bugs found before a user sees them. Bugs found by users may result in lost users, lost time and theft. These all hurt the bottom line. At the same time, your users are a lot more effective at finding bugs than you are due to sheer numbers if nothing else, and as we've established it's basically impossible to ship fully correct software, so we end up choosing some level of acceptable defect rate in the middle. This is generally determined by the point at which it is more expensive to find the next bug yourself than it is to let your users find it. Any higher or lower defect rate and you could just adjust your development process and make more money, and companies like making money so if they're competently run will generally do the things that cause them to do so. You can, and should, [cheaper to find bugs is to reduce the cost of when your users do find them](http://itamarst.org/softwaretesting/book/realworld.html), but it's always going to be expensive. This means that there are only two viable ways to improve software quality: * Make users angrier about bugs * Make it cheaper to find bugs I think making users angrier about bugs is a good idea and I wish people cared more about software quality, but as a business plan it's a bit of a rubbish one. It creates higher quality software by making it more expensive to write software. Making it cheaper to find bugs though... that's a good one, because it increases the quality of the software by increasing your profit margins. Literally everyone wins: The developers win, the users win, the business's owners win. And so this is the lever we get to pull to change the world: If you want better software, make or find tools that reduce the effort of finding bugs. Obviously I think Hypothesis is an example of this, but it's neither the only one nor the only one you need. Better monitoring is another. Code review processes. Static analysis. Improved communication. There are many more. But one thing that *won't* improve your ability to find bugs is feeling bad about yourself and trying really hard to write correct software then feeling guilty when you fail. This seems to be the current standard, and it's deeply counter-productive. You can't fix systemic issues with individual action, and the only way to ship better software is to change the economics to make it viable to do so. ================================================ FILE: website/content/2016-04-15-getting-started-with-hypothesis.md ================================================ --- tags: intro, python, technical, properties date: 2016-04-15 13:00 title: Getting started with Hypothesis author: drmaciver --- Hypothesis will speed up your testing process and improve your software quality, but when first starting out people often struggle to figure out exactly how to use it. Until you're used to thinking in this style of testing, it's not always obvious what the invariants of your code actually *are*, and people get stuck trying to come up with interesting ones to test. Fortunately, there's a simple invariant which every piece of software should satisfy, and which can be remarkably powerful as a way to uncover surprisingly deep bugs in your software. That invariant is simple: The software shouldn't crash. Or sometimes, it should only crash in defined ways. There is then a standard test you can write for most of your code that asserts this invariant. It consists of two steps: 1. Pick a function in your code base that you want to be better tested. 2. Call it with random data. This style of testing is usually called *fuzzing*. This will possibly require you to figure out how to generate your domain objects. Hypothesis [has a pretty extensive library](../generating-the-right-data/) of tools (called 'strategies' in Hypothesis terminology) for generating custom types but if you can, try to start somewhere where the types you need aren’t *too* complicated to generate. Chances are actually pretty good that you’ll find something wrong this way if you pick a sufficiently interesting entry point. For example, there’s a long track record of people trying to test interesting properties with their text handling and getting unicode errors when text() gives them something that their code didn’t know how to handle. You’ll probably get exceptions here you don’t care about. e.g. some arguments to functions may not be valid. Set up your test to ignore those. So at this point you’ll have something like this: ```python from hypothesis import given, reject from hypothesis.strategies import integers, text @given(integers(), text()) def test_some_stuff(x, y): try: my_function(x, y) except SomeExpectedException: reject() ``` In this example we generate two values - one integer, one text - and pass them to your test function. Hypothesis will repeatedly call the test function with values drawn from these strategies, trying to find one that produces an unexpected exception. When an exception we know is possible happens (e.g. a ValueError because some argument was out of range) we call reject. This discards the example, and Hypothesis won't count it towards the 'budget' of examples it is allowed to run. This is already a pretty good starting point and does have a decent tendency to flush out bugs. You’ll often find cases where you forgot some boundary condition and your code misbehaves as a result. But there’s still plenty of room to improve. There are now two directions you can go in from here: 1. Try to assert some things about the function’s result. Anything at all. What type is it? Can it be None? Does it have any relation at all to the input values that you can check? It doesn’t have to be clever - even very trivial properties are useful here. 2. Start making your code more defensive. The second part is probably the most productive one. The goal is to turn faulty assumptions in your code into crashes instead of silent corruption of your application state. You can do this in a couple ways: 1. Add argument checking to your functions (Hypothesis uses a dedicated InvalidArgumentException for this case, but you can raise whatever errors you find appropriate). 2. Add a whole bunch of assertions into your code itself. Even when it’s hard to reason about formal properties of your code, it’s usually relatively easy to add local properties, and assertions are a great way to encode them. John Regehr has [a good post on this subject](http://blog.regehr.org/archives/1091) if you want to know more about it. This approach will make your code more robust even if you don’t find any bugs in it during testing (and you’ll probably find bugs in it during testing), and it gives you a nice easy route into property based testing by letting you focus on only one half of the problem at a time. Once you think you've got the hang of this, a good next step is to start looking for [places with complex optimizations](../testing-performance-optimizations/) or [Encode/Decode pairs](../encode-decode-invariant/) in your code, as they're a fairly easy properties to test and are both rich sources of bugs. ================================================ FILE: website/content/2016-04-16-anatomy-of-a-test.md ================================================ --- tags: python, details, technical date: 2016-04-16 06:00 title: Anatomy of a Hypothesis Based Test author: drmaciver --- What happens when you run a test using Hypothesis? This article will help you understand. The Python version of Hypothesis uses *decorators* to transform a test function that doesn't use Hypothesis into one that does. Consider the following example using [py.test](http://pytest.org/latest/) style testing: ```python from hypothesis import given from hypothesis.strategies import floats @given(floats(), floats()) def test_floats_are_commutative(x, y): assert x + y == y + x ``` The inner function takes two arguments, but the wrapping function defined by the @given decorator takes none and may be invoked as a normal test: ```bash python -m pytest test_floats.py ``` And we see the following output from py.test: ``` @given(floats(), floats()) def test_floats_are_commutative(x, y): > assert x + y == y + x E assert (0.0 + nan) == (nan + 0.0) test_floats.py:7: AssertionError Falsifying example: test_floats_are_commutative(x=0.0, y=nan) ``` The test fails, because [nan](https://en.wikipedia.org/wiki/NaN) is a valid floating point number which is not equal to itself, and adding anything to nan yields nan. When we ran this, Hypothesis invoked our test function with a number of randomly chosen values for the arguments until it found one that failed. It then attempted to *shrink* those values to produce a simpler one that would also fail. If we wanted to see what it actually called our function with we can set the *verbosity level*. This can either be done in code with settings, or by specifying an environment variable: ```python from hypothesis import Verbosity, given, settings from hypothesis.strategies import floats @settings(verbosity=Verbosity.verbose) @given(floats(), floats()) def test_floats_are_commutative(x, y): assert x + y == y + x ``` ```bash HYPOTHESIS_VERBOSITY_LEVEL=verbose python -m pytest test_floats.py ``` Any verbosity values explicitly passed in settings will override whatever is set at the environment level - the latter just provides a default. Whichever one we choose, running it we'll see output something like the following: ``` Trying example: test_floats_are_commutative(x=-0.05851890381391768, y=-6.060045836901702e+300) Trying example: test_floats_are_commutative(x=-0.06323690311413645, y=2.0324087421708266e-308) Trying example: test_floats_are_commutative(x=-0.05738038380011458, y=-1.5993500302384265e-308) Trying example: test_floats_are_commutative(x=-0.06598754758697359, y=-1.1412902232349034e-308) Trying example: test_floats_are_commutative(x=-0.06472919559855002, y=1.7429441378277974e+35) Trying example: test_floats_are_commutative(x=-0.06537123121982172, y=-8.136220566134233e-156) Trying example: test_floats_are_commutative(x=-0.06016703321602157, y=1.9718842567475311e-215) Trying example: test_floats_are_commutative(x=-0.055257588875432875, y=1.578407827448836e-308) Trying example: test_floats_are_commutative(x=-0.06313031758042666, y=1.6749023021600297e-175) Trying example: test_floats_are_commutative(x=-0.05886897920547916, y=1.213699633272585e+292) Trying example: test_floats_are_commutative(x=-12.0, y=-0.0) Trying example: test_floats_are_commutative(x=4.0, y=1.7976931348623157e+308) Trying example: test_floats_are_commutative(x=-9.0, y=0.0) Trying example: test_floats_are_commutative(x=-38.0, y=1.7976931348623157e+308) Trying example: test_floats_are_commutative(x=-24.0, y=1.5686642754811104e+289) Trying example: test_floats_are_commutative(x=-10.0, y=nan) Traceback (most recent call last): ... AssertionError: assert (-10.0 + nan) == (nan + -10.0) Trying example: test_floats_are_commutative(x=10.0, y=nan) Traceback (most recent call last): ... AssertionError: assert (10.0 + nan) == (nan + 10.0) Trying example: test_floats_are_commutative(x=0.0, y=nan) Traceback (most recent call last): ... AssertionError: assert (0.0 + nan) == (nan + 0.0) Trying example: test_floats_are_commutative(x=0.0, y=0.0) Trying example: test_floats_are_commutative(x=0.0, y=inf) Trying example: test_floats_are_commutative(x=0.0, y=-inf) Falsifying example: test_floats_are_commutative(x=0.0, y=nan) ``` Notice how the first failing example we got was -10.0, nan but Hypothesis was able to turn that into 0.0, nan. That's the shrinking at work. For a simple case like this it doesn't matter so much, but as your examples get complicated it's essential for making Hypothesis's output easy to understand. ``` Trying example: test_floats_are_commutative(x=nan, y=0.0) Traceback (most recent call last): ... AssertionError: assert (nan + 0.0) == (0.0 + nan) Trying example: test_floats_are_commutative(x=0.0, y=0.0) Trying example: test_floats_are_commutative(x=inf, y=0.0) Trying example: test_floats_are_commutative(x=-inf, y=0.0) Falsifying example: test_floats_are_commutative(x=nan, y=0.0) ``` Now lets see what happens when we rerun the test: ``` Trying example: test_floats_are_commutative(x=0.0, y=nan) Traceback (most recent call last): ... AssertionError: assert (0.0 + nan) == (nan + 0.0) Trying example: test_floats_are_commutative(x=0.0, y=0.0) Trying example: test_floats_are_commutative(x=0.0, y=inf) Trying example: test_floats_are_commutative(x=0.0, y=-inf) Falsifying example: test_floats_are_commutative(x=0.0, y=nan) ``` Notice how the first example it tried was the failing example we had last time? That's not an accident. Hypothesis has an example database where it saves failing examples. When it starts up it looks for any examples it has seen failing previously and tries them first before any random generation occurs. If any of them fail, we take that failure as our starting point and move straight to the shrinking phase without any generation. The database format is safe to check in to version control if you like and will merge changes correctly out of the box, but it's often clearer to specify the examples you want to run every time in the source code as follows: ```python from hypothesis import example, given from hypothesis.strategies import floats @example(0.0, float("nan")) @given(floats(), floats()) def test_floats_are_commutative(x, y): assert x + y == y + x ``` ``` Falsifying example: test_floats_are_commutative(x=0.0, y=nan) ``` If you run this in verbose mode it will print out Falsifying example: test\_floats\_are\_commutative(x=0.0, y=nan) immediately and not try to do any shrinks. Values you pass in via example will not be shrunk. This is partly a technical limitation but it can often be useful as well. Explicitly provided examples are run before any generated examples. So, to recap and elaborate, when you use a test written using Hypothesis: 1. Your test runner sees the decorated test as if it were a perfectly normal test function and invokes it. 2. Hypothesis calls your test function with each explicitly provided @example. If one of these fails it stops immediately and bubbles up the exception for the test runner to handle. 3. Hypothesis reads examples out of its database of previously failing examples. If any of them fail, it stops there and proceeds to the shrinking step with that example. Otherwise it continues to the generation step. 4. Hypothesis tries generating a number of examples. If any of these raises an exception, it stops there and proceeds to the shrinking step. If none of them raise an exception, it silently returns and the test passes. 5. Hypothesis takes the previously failing example it's seen and tries to produce a "Simpler" version of it. Once it has found the simplest it possibly can, it saves that in the example database (in actual fact it saves every failing example in the example database as it shrinks, but the reasons why aren't important right now). 6. Hypothesis takes the simplest failing example and replays it, finally letting the test bubble up to the test runner. ================================================ FILE: website/content/2016-04-16-encode-decode-invariant.md ================================================ --- tags: python, intro, technical, properties date: 2016-04-16 06:00 title: The Encode/Decode invariant author: drmaciver --- One of the simplest types of invariant to find once you move past [just fuzzing your code](../getting-started-with-hypothesis/) is asserting that two different operations should produce the same result, and one of the simplest instances of *that* is looking for encode/decode pairs. That is, you have some function that takes a value and encodes it as another value, and another that is supposed to reverse the process. This is ripe for testing with Hypothesis because it has a natural completely defined specification: Encoding and then decoding should be exactly the same as doing nothing. Let's look at a concrete example. The following code is a lightly reformatted version of an implementation of [Run Length Encoding](https://en.wikipedia.org/wiki/Run-length_encoding) taken [from Rosetta Code](http://rosettacode.org/wiki/Run-length_encoding). ```python def encode(input_string): count = 1 prev = "" lst = [] for character in input_string: if character != prev: if prev: entry = (prev, count) lst.append(entry) count = 1 prev = character else: count += 1 else: entry = (character, count) lst.append(entry) return lst def decode(lst): q = "" for character, count in lst: q += character * count return q ``` We can test this using Hypothesis and py.test as follows: ```python from hypothesis import given from hypothesis.strategies import text @given(text()) def test_decode_inverts_encode(s): assert decode(encode(s)) == s ``` This asserts what we described above: If we encode a string as run length encoded and then decode it, we get back to where we started. This test finds a bug, not through the actual invariant. Instead it finds one through pure fuzzing: The code does not correctly handle the empty string. ``` Falsifying example: test_decode_inverts_encode(s='') UnboundLocalError: local variable 'character' referenced before assignment ``` One of the nice features of testing invariants is that they incorporate the fuzzing you could be doing anyway, more or less for free, so even trivial invariants can often find interesting problems. We can fix this bug by adding a guard to the encode function: ```python if not input_string: return [] ``` The test now passes, which isn't very interesting, so let's break the code. We'll delete a line from our implementation of encode which resets the count when the character changes: ```python def encode(input_string): if not input_string: return [] count = 1 prev = "" lst = [] for character in input_string: if character != prev: if prev: entry = (prev, count) lst.append(entry) # count = 1 # Missing reset operation prev = character else: count += 1 else: entry = (character, count) lst.append(entry) return lst ``` Now the test fails: ``` @given(text()) def test_decode_inverts_encode(s): > assert decode(encode(s)) == s E assert '1100' == '110' E - 1100 E ? - E + 110 test_encoding.py:35: AssertionError ------------------------------------ Hypothesis ------------------------------------ Falsifying example: test_decode_inverts_encode(s='110') ``` Not resetting the count did indeed produce unintended data that doesn't translate back to the original thing. Hypothesis has given us the shortest example that could trigger it - two identical characters followed by one different one. It's not *quite* the simplest example according to Hypothesis's preferred ordering - that would be '001' - but it's still simple enough to be quite legible, which helps to rapidly diagnose the problem when you see it in real code. Encode/decode loops like this are *very* common, because you will frequently want to serialize your domain objects to other representations - into forms, into APIs, into the database, and these are things that are so integral to your applications that it's worth getting all the edge cases right. Other examples of this: * [This talk by Matt Bacchman](https://speakerdeck.com/bachmann1234/property-based-testing-hypothesis) in which he discovers an eccentricity of formats for dates. * Mercurial bugs [4927](https://bz.mercurial-scm.org/show_bug.cgi?id=4927) and [5031](https://bz.mercurial-scm.org/show_bug.cgi?id=5031) were found by applying this sort of testing to their internal UTF8b encoding functions. * [This test](https://github.com/The-Compiler/qutebrowser/blob/24a71e5c2ebbffd9021694f32fa9ec51d0046d5a/tests/unit/browser/test_webelem.py#L652). Has caught three bugs in Qutebrowser's JavaScript escaping ([1](https://github.com/The-Compiler/qutebrowser/commit/73e9fd11188ce4dddd7626e39d691e0df649e87c), [2](https://github.com/The-Compiler/qutebrowser/commit/93d27cbb5f49085dd5a7f5e05f2cc45cc84f94a4), [3](https://github.com/The-Compiler/qutebrowser/commit/24a71e5c2ebbffd9021694f32fa9ec51d0046d5a)), which could have caused data loss if a user had run into them. ================================================ FILE: website/content/2016-04-16-quickcheck-in-every-language.md ================================================ --- tags: alternatives, technical date: 2016-04-16 15:00 title: QuickCheck in Every Language author: drmaciver ---

There are a lot of ports of QuickCheck, the original property based testing library, to a variety of different languages.

Some of them are good. Some of them are very good. Some of them are OK. Many are not.

I thought it would be worth keeping track of which are which, so I've put together a list.

In order to make it onto this list, an implementation has to meet the following criteria:

  1. Must support random generation of data to a test function. e.g. testing systems based on smallcheck while interesting and related don't fit on this list.
  2. It must be fairly straightforward to generate your own custom types.
  3. It must support shrinking of falsifying examples.
  4. It must be under active development, in the sense that bugs in it will get fixed.
  5. Must be under an OSI approved license.

In this I've tried to collect a list of what I think the best ones are for any given language. I haven't used all of these, but I've used some and read and talked to people about the others.

Uncontested Winners by Language

For many languages there is a clear winner that you should just use. Here are the ones I've found and what I think of them.

Language Library Our rating
C theft Good but does not come with a library of data generators.
C++ CppQuickCheck Unsure
Clojure test.check Very good
Coq QuickChick Unsure
F# FsCheck Very Good
Go gopter Unsure but looks promising.
Haskell Hedgehog Comparatively new, but looks solid. See below.
Java QuickTheories Unsure. Extremely new but looks promising.
JavaScript jsverify Good
PHP Eris Unsure. Looks promising.
Python Hypothesis I may be a bit biased on this one, but it's also unambiguously true.
Ruby Rantly Unsure. We're not convinced, but the alternatives are definitely worse.
Rust Quickcheck Unsure, but probably very good based on initial appearance and usage level.
Scala ScalaCheck Very Good
Swift Swiftcheck Unsure

Where when I've said "Unsure" I really just mean that I think it looks good but I haven't put in the depth of in time to be sure, not that I have doubts.

Special case: Haskell

The original QuickCheck was of course written in Haskell, so it may seem odd that it's not the property based testing library I recommend for Haskell!

The reason is that I feel that the design of classic QuickCheck is fundamentally limiting, and that Hedgehog takes it in the direction that the rest of the property-based testing world is moving (and where most of the implementations for dynamic languages, Hypothesis included, already are). In particular its approach starts from generators rather than type classes, and it has integrated shrinking, and a fairly comprehensive library of generators.

Special case: Erlang

Erlang is a special case because they have QuviQ's QuickCheck. Their QuickCheck implementation is by all accounts extremely good, but it is also proprietary and fairly expensive. Nevertheless, if you find yourselves in the right language, circumstance and financial situation to use it, I would strongly recommend doing so.

In particular, QuviQ's QuickCheck is really the only implementation in this article I think is simply better than Hypothesis. Hypothesis is significantly more user friendly, especially if the users in question are less than familiar with Erlang, but there are things QuviQ can do that Hypothesis can't, and the data generation has had a great deal more engineering effort put into it.

If you're using Erlang but not able to pay for QuickCheck, apparently the one to use is PropEr. If you're also unable to use GPLed software there's triq. I know very little about either.

Special case: OCaml

OCaml seems to be suffering from a problem of being close enough to Haskell that people try to do a straight port of Quickcheck but far enough from Haskell that this doesn't work. The result is that there is a "mechanical port" of Quickcheck which is completely abandoned and a fork of it that uses more idiomatic OCaml. I'm insufficiently familiar with OCaml or its community to know if either is used or whether there is another one that is.

What does this have to do with Hypothesis?

In some sense these are all "competitors" to Hypothesis, but we're perfectly happy not to compete.

In the case of Erlang, I wouldn't even try. In the case of Scala, F#, or Clojure, I might at some point work with them to try to bring the best parts of Hypothesis to their existing implementation, but I don't consider them a priority - they're well served by what they have right now, and there are many languages that are not.

For the rest though? I'm glad they exist! I care about testing and about high quality software, and they're doing their part to make it possible.

But I feel they're being badly served by their underlying model, and that they feel quite unnatural to use in the context of a more conventional test setting. I think Hypothesis is the way forward, and I'll be doing my best to make it possible for everyone to use it in their language of choice.

================================================ FILE: website/content/2016-04-16-the-purpose-of-hypothesis.md ================================================ --- tags: writing-good-software, principles, non-technical date: 2016-04-16 12:00 title: The Purpose of Hypothesis author: drmaciver --- What is Hypothesis for? From the perspective of a user, the purpose of Hypothesis is to make it easier for you to write better tests. From my perspective as the primary author, that is of course also *a* purpose of Hypothesis. I write a lot of code, it needs testing, and the idea of trying to do that without Hypothesis has become nearly unthinkable. But, on a large scale, the true purpose of Hypothesis is to drag the world kicking and screaming into a new and terrifying age of high quality software. Software is everywhere. We have built a civilization on it, and it's only getting more prevalent as more services move online and embedded and "internet of things" devices become cheaper and more common. Software is also terrible. It’s buggy, it's insecure, and it's rarely well thought out. This combination is clearly a recipe for disaster. The state of software testing is even worse. It’s uncontroversial at this point that you *should* be testing your code, but it's a rare codebase whose authors could honestly claim that they feel its testing is sufficient. Much of the problem here is that it’s too hard to write good tests. Tests take up a vast quantity of development time, but they mostly just laboriously encode exactly the same assumptions and fallacies that the authors had when they wrote the code, so they miss exactly the same bugs that you missed when they wrote the code. Meanwhile, there are all sorts of tools for making testing better that are basically unused, or used in only specialised contexts. The original Quickcheck is from 1999 and the majority of developers have not even heard of it, let alone used it. There are a bunch of half-baked implementations for most languages, but very few of them are worth using. More recently, there are many good tools applied to specialized problems, but very little that even attempts, let alone succeeds, to help general purpose testing. The goal of Hypothesis is to fix this, by taking research level ideas and applying solid engineering to them to produce testing tools that are both powerful *and* practical, and are accessible to everyone.. Many of the ideas that Hypothesis is built on are new. Many of them are not. It doesn't matter. The purpose of Hypothesis is not to produce research level ideas. The purpose of Hypothesis is to produce high quality software by any means necessary. Where the ideas we need exist, we will use them. Where they do not, we will invent them. If people aren't using advanced testing tools, that's a bug. We should find it and fix it. Fortunately, we have this tool called Hypothesis. It's very good at finding bugs. But this one it can also fix. ================================================ FILE: website/content/2016-04-19-rule-based-stateful-testing.md ================================================ --- tags: python, technical, intro date: 2016-04-19 07:00 title: Rule Based Stateful Testing author: drmaciver --- Hypothesis's standard testing mechanisms are very good for testing things that can be considered direct functions of data. But supposed you have some complex stateful system or object that you want to test. How can you do that? In this article we'll see how to use Hypothesis's *rule based state machines* to define tests that generate not just simple data, but entire programs using some stateful object. These will give the same level of boost to testing the behaviour of the object as you get to testing the data it accepts. The model of a stateful system we'll be using is a [priority queue](https://en.wikipedia.org/wiki/Priority_queue) implemented as a binary heap. We have the following operations: * newheap() - returns a new heap * heappush(heap, value) - place a new value into the heap * heappop(heap) - remove and return the smallest value currently on the heap. Error if heap is empty. * heapempty(heap) - return True if the heap has no elements, else False. We'll use the following implementation of these: ```python def heapnew(): return [] def heapempty(heap): return not heap def heappush(heap, value): heap.append(value) index = len(heap) - 1 while index > 0: parent = (index - 1) // 2 if heap[parent] > heap[index]: heap[parent], heap[index] = heap[index], heap[parent] index = parent else: break def heappop(heap): return heap.pop(0) ``` (Note that this implementation is *wrong*. heappop as implemented will return the smallest element if the heap currently satisfies the heap property, but it will not rebalance the heap afterwards so it may leave the heap in an invalid state) We could test this readily enough using @given with something like the following: ```python from hypothesis import given from hypothesis.strategies import integers, lists @given(lists(integers())) def test_pop_in_sorted_order(ls): h = heapnew() for l in ls: heappush(h, l) r = [] while not heapempty(h): r.append(heappop(h)) assert r == sorted(ls) ``` And this indeed finds the bug: ``` > assert r == sorted(ls) E assert [0, 1, 0] == [0, 0, 1] E At index 1 diff: 1 != 0 E Use -v to get the full diff binheap.py:74: AssertionError ----- Hypothesis ----- Falsifying example: test_pop_in_sorted_order(ls=[0, 1, 0]) ``` So we replace heappop with a correct implementation which rebalances the heap: ```python def heappop(heap): if len(heap) == 0: raise ValueError("Empty heap") if len(heap) == 1: return heap.pop() result = heap[0] heap[0] = heap.pop() index = 0 while index * 2 + 1 < len(heap): children = [index * 2 + 1, index * 2 + 2] children = [i for i in children if i < len(heap)] assert children children.sort(key=lambda x: heap[x]) for c in children: if heap[index] > heap[c]: heap[index], heap[c] = heap[c], heap[index] index = c break else: break return result ``` But how do we know this is enough? Might some combination of mixing pushes and pops break the invariants of the heap in a way that this simple pattern of pushing everything then popping everything cannot witness? This is where the rule based state machines come in. Instead of just letting Hypothesis give us data which we feed into a fixed structure of test, we let Hypothesis choose which operations to perform on our data structure: ```python from hypothesis.stateful import RuleBasedStateMachine, precondition, rule class HeapMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.heap = [] @rule(value=integers()) def push(self, value): heappush(self.heap, value) @rule() @precondition(lambda self: self.heap) def pop(self): correct = min(self.heap) result = heappop(self.heap) assert correct == result ``` @rule is a slightly restricted version of @given that only works for methods on a RuleBasedStateMachine. However it has one *major* difference from @given, which is that multiple rules can be chained together: A test using this state machine doesn't just run each rule in isolation, it instantiates an instance of the machine and then runs multiple rules in succession. The @precondition decorator constrains when a rule is allowed to fire: We are not allowed to pop from an empty heap, so the pop rule may only fire when there is data to be popped. We can run this by getting a standard unit test TestCase object out of it to be picked up by unittest or py.test as normal: ```python TestHeaps = HeapMachine.TestCase ``` With our original broken heappop we find the same bug as before: ``` E AssertionError: assert 0 == 1 binheap.py:90: AssertionError ----- Captured stdout call ----- Step #1: push(value=1) Step #2: push(value=0) Step #3: push(value=0) Step #4: pop() Step #5: pop() ``` With the fixed implementation the test passes. As it currently stands, this is already very useful. It's particularly good for testing single standalone objects or services like storage systems. But one limitation of it as we have written it is that it only concerns ourselves with a single heap. What if we wanted to combine two heaps? For example, suppose we wanted a heap merging operation that takes two heaps and returns a new heap containing the values in either of the original two. As before, we'll start with a broken implementation: ```python def heapmerge(x, y): x, y = sorted((x, y)) return x + y ``` We can't just write a strategy for heaps, because each heap would be a fresh object and thus it would not preserve the stateful aspect. What we instead do is use the other big feature of Hypothesis's rule bases state machines: Bundles. Bundles allow rules to return as well as accept values. A bundle is a strategy which generates anything a rule has previously provided to it. Using them is as follows: ```python class HeapMachine(RuleBasedStateMachine): Heaps = Bundle("heaps") @rule(target=Heaps) def newheap(self): return [] @rule(heap=Heaps, value=integers()) def push(self, heap, value): heappush(heap, value) @rule(heap=Heaps.filter(bool)) def pop(self, heap): correct = min(heap) result = heappop(heap) assert correct == result @rule(target=Heaps, heap1=Heaps, heap2=Heaps) def merge(self, heap1, heap2): return heapmerge(heap1, heap2) ``` So now instead of a single heap we manage a collection of heaps. All of our previous operations become constrained by an instance of a heap. Note the use of filter: A bundle is a strategy you can use like any other. In this case the filter replaces our use of a precondition because we now only care about whether this *specific* heap is empty. This is sufficient to find the fact that our implementation is wrong: ``` @rule(heap=Heaps.filter(bool)) def pop(self, heap): correct = min(heap) result = heappop(heap) > assert correct == result E AssertionError: assert 0 == 1 binheap.py:105: AssertionError ----- Captured stdout call ----- Step #1: v1 = newheap() Step #2: push(heap=v1, value=0) Step #3: push(heap=v1, value=1) Step #4: push(heap=v1, value=1) Step #5: v2 = merge(heap2=v1, heap1=v1) Step #6: pop(heap=v2) Step #7: pop(heap=v2) ``` We create a small heap, merge it with itself, and rapidly discover that it has become unbalanced. We can fix this by fixing our heapmerge to be correct: ```python def heapmerge(x, y): result = list(x) for v in y: heappush(result, v) return result ``` But that's boring. Lets introduce a more *interestingly* broken implementation instead: ```python def heapmerge(x, y): result = [] i = 0 j = 0 while i < len(x) and j < len(y): if x[i] <= y[j]: result.append(x[i]) i += 1 else: result.append(y[j]) j += 1 result.extend(x[i:]) result.extend(y[j:]) return result ``` This merge operation selectively splices two heaps together as if we were merging two sorted lists (heaps aren't actually sorted, but the code still works regardless it just doesn't do anything very meaningful). This is wrong, but it turns out to work surprisingly well for small heaps and it's not completely straightforward to find an example showing that it's wrong. Here's what Hypothesis comes up with: ``` Step #1: v1 = newheap() Step #2: push(heap=v1, value=0) Step #3: v2 = merge(heap1=v1, heap2=v1) Step #4: v3 = merge(heap1=v2, heap2=v2) Step #5: push(heap=v3, value=-1) Step #6: v4 = merge(heap1=v1, heap2=v2) Step #7: pop(heap=v4) Step #8: push(heap=v3, value=-1) Step #9: v5 = merge(heap1=v1, heap2=v2) Step #10: v6 = merge(heap1=v5, heap2=v4) Step #11: v7 = merge(heap1=v6, heap2=v3) Step #12: pop(heap=v7) Step #13: pop(heap=v7) ``` Through a careful set of heap creation and merging, Hypothesis manages to find a series of merges that produce an unbalanced heap. Every heap prior to v7 is balanced, but v7 looks like this: ``` >>> v7 [-1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0] ``` Which doesn't satisfy the heap property because of that -1 far down in the list. I don't know about you, but I would never have come up with that example. There's probably a simpler one given a different set of operations - e.g. one thing that would probably improve the quality of this test is to let Hypothesis instantiate a new heap with a list of elements which it pops onto it. But the nice thing about rule based stateful testing is that I don't *have* to come up with the example. Instead Hypothesis is able to guarantee that every combination of operations on my objects works, and can flush out some remarkably subtle bugs in the process. Because after all, if it takes this complicated an example to demonstrate that a completely wrong implementation is wrong, how hard can it sometimes be to demonstrate subtle bugs? ### Real world usage This feature is currently somewhat under-documented so hasn't seen as widespread adoption as it could. However, there are at least two interesting real world examples: 1. Hypothesis uses it to test itself. Hypothesis has [tests of its example database]( https://github.com/HypothesisWorks/hypothesis-python/blob/master/tests/cover/test_database_agreement.py) which work very much like the above, and [a small model of its test API]( https://github.com/HypothesisWorks/hypothesis-python/blob/master/tests/nocover/test_strategy_state.py) which generates random strategies and runs tests using them. 2. It's being used to [test Mercurial](https://www.mercurial-scm.org/pipermail/mercurial-devel/2016-February/080037.html) generating random. So far it's found [bug 5112](https://bz.mercurial-scm.org/show_bug.cgi?id=5112) and [bug 5113](https://bz.mercurial-scm.org/show_bug.cgi?id=5113). The usage pattern on Mercurial is one such that the stateful testing probably needs more resources, more rules and more work on deployment before it's going to find much more than that though. ================================================ FILE: website/content/2016-04-29-testing-performance-optimizations.md ================================================ --- tags: technical, intro, python, properties date: 2016-04-29 11:00 title: Testing performance optimizations author: drmaciver --- Once you've [flushed out the basic crashing bugs](../getting-started-with-hypothesis/) in your code, you're going to want to look for more interesting things to test. The next easiest thing to test is code where you know what the right answer is for every input. Obviously in theory you think you know what the right answer is - you can just run the code. That's not very helpful though, as that's the answer you're trying to verify. But sometimes there is more than one way to get the right answer, and you choose the one you run in production not because it gives a different answer but because it gives the same answer *faster*. For example: * There might be a fancy but fast version of an algorithm and a simple but slow version of an algorithm. * You might have a caching layer and be able to run the code with and without caching turned on, or with a different cache timeout. * You might be moving to a new database backend to improve your scalability, but you still have the code for the old backend until you've completed your migration. There are plenty of other ways this can crop up, but those are the ones that seem the most common. Anyway, this creates an *excellent* use case for property based testing, because if two functions are supposed to always return the same answer, you can test that: Just call both functions with the same data and assert that their answer is the same. Let's look at this in the fancy algorithm case. Suppose we implemented [merge sort]( https://en.wikipedia.org/wiki/Merge_sort): ```python def merge_sort(ls): if len(ls) <= 1: return ls else: k = len(ls) // 2 return merge_sorted_lists(merge_sort(ls[:k]), merge_sort(ls[k:])) def merge_sorted_lists(x, y): result = [] i = 0 j = 0 while i < len(x) and j < len(y): if x[i] <= y[j]: result.append(x[i]) i += 1 else: result.append(y[j]) j += 1 return result ``` We want a reference implementation to test it against, so lets also implement [bubble sort]( https://en.wikipedia.org/wiki/Bubble_sort): ```python def bubble_sort(ls): ls = list(ls) needs_sorting = True while needs_sorting: needs_sorting = False for i in range(1, len(ls)): if ls[i - 1] > ls[i]: needs_sorting = True ls[i - 1], ls[i] = ls[i], ls[i - 1] return ls ``` These *should* always give the same answer, so let's test that: ```python @given(lists(integers())) def test_bubble_sorting_is_same_as_merge_sorting(ls): assert bubble_sort(ls) == merge_sort(ls) ``` This gives us an error: ``` @given(lists(integers())) def test_bubble_sorting_is_same_as_merge_sorting(ls): > assert bubble_sort(ls) == merge_sort(ls) E assert [0, 0] == [0] E Left contains more items, first extra item: 0 E Use -v to get the full diff foo.py:43: AssertionError ----- Hypothesis ----- Falsifying example: test_bubble_sorting_is_same_as_merge_sorting(ls=[0, 0]) ``` What's happened is that we messed up our implementation of merge\_sorted\_lists, because we forgot to include the elements left over in the other list once we've reached the end of one of them. As a result we ended up losing elements from the list, a problem that our simpler implementation lacks. We can fix this as follows and then the test passes: ```python def merge_sorted_lists(x, y): result = [] i = 0 j = 0 while i < len(x) and j < len(y): if x[i] <= y[j]: result.append(x[i]) i += 1 else: result.append(y[j]) j += 1 result.extend(x[i:]) result.extend(y[j:]) return result ``` This technique combines especially well with [Hypothesis's stateful testing](../rule-based-stateful-testing/), because you can use it to then test different implementations of complex APIs. For example, Hypothesis uses this property together with stateful testing to [verify that the different implementations of its example database behave identically](https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/tests/nocover/test_database_agreement.py). ================================================ FILE: website/content/2016-05-02-referential-transparency.md ================================================ --- tags: non-technical date: 2016-05-02 08:00 title: You Don't Need Referential Transparency author: drmaciver --- It's a common belief that in order for property based testing to be useful, your code must be [referentially transparent](https://en.wikipedia.org/wiki/Referential_transparency). That is, it must be a pure function with no side effects that just takes input data and produces output data and is solely defined by what input data produces what output data. This is, bluntly, complete and utter nonsense with no basis in reality. The idea comes from the fact that it was true of very early versions of [the original Haskell QuickCheck](https://hackage.haskell.org/package/QuickCheck) - it was designed to look more like formal methods than unit testing, and it was designed for a language where referential transparency was the norm. But that was the *original* version of Haskell QuickCheck. It's not even true for the latest version of it, let alone for ports to other languages! The Haskell version has full support for testing properties in IO (if you don't know Haskell, this means "tests which may have side effects"). It works really well. Hypothesis doesn't even consider this a question - testing code with side effects works the same way as testing code without side effects. The *only* requirement that property based testing has on the side effects your tests may perform is that if your test has *global* side effects then it must be able to roll them back at the end. If that sounds familiar, it's because it's *exactly the same requirement every other test has*. Tests that have global side effects are not repeatable and may interfere with other tests, so they must keep their side effects to themselves by rolling them back at the end of the test. Property based testing is just normal testing, run multiple times, with a source of data to fill in some of the blanks. There is no special requirement on it beyond that, and the myth that there is causes great harm and keeps many people from adopting more powerful testing tools. ================================================ FILE: website/content/2016-05-11-generating-the-right-data.md ================================================ --- tags: technical, python, intro date: 2016-05-11 10:00 title: Generating the right data author: drmaciver --- One thing that often causes people problems is figuring out how to generate the right data to fit their data model. You can start with just generating strings and integers, but eventually you want to be able to generate objects from your domain model. Hypothesis provides a lot of tools to help you build the data you want, but sometimes the choice can be a bit overwhelming. Here's a worked example to walk you through some of the details and help you get to grips with how to use them. Suppose we have the following class: ```python class Project: def __init__(self, name, start, end): self.name = name self.start = start self.end = end def __repr__(self): return f"Project {self.name} from {self.start.isoformat()} to {self.end.isoformat()}" ``` A project has a name, a start date, and an end date. How do we generate such a thing? The idea is to break the problem down into parts, and then use the tools Hypothesis provides to assemble those parts into a strategy for generating our projects. We'll start by generating the data we need for each field, and then at the end we'll see how to put it all together to generate a Project. ### Names First we need to generate a name. We'll use Hypothesis's standard text strategy for that: ```pycon >>> from hypothesis.strategies import text >>> text().example() '' >>> text().example() '\nŁ昘迥' ``` Lets customize this a bit: First off, lets say project names have to be non-empty. ```pycon >>> text(min_size=1).example() 'w\nC' >>> text(min_size=1).example() 'ሚಃJ»' ``` Now, lets avoid the high end unicode for now (of course, your system *should* handle the full range of unicode, but this is just an example, right?). To do this we need to pass an alphabet to the text strategy. This can either be a range of characters or another strategy. We're going to use the *characters* strategy, which gives you a flexible way of describing a strategy for single-character text strings, to do that. ```pycon i>>> characters(min_codepoint=1, max_codepoint=1000, exclude_categories=('Cc', 'Cs')).example() '²' >>> characters(min_codepoint=1, max_codepoint=1000, exclude_categories=('Cc', 'Cs')).example() 'E' >>> characters(min_codepoint=1, max_codepoint=1000, exclude_categories=('Cc', 'Cs')).example() '̺' ``` The max and min codepoint parameters do what you'd expect: They limit the range of permissible codepoints. We've blocked off the 0 codepoint (it's not really useful and tends to just cause trouble with C libraries) and anything with a codepoint above 1000 - so we're considering non-ASCII characters but nothing really high end. The blacklist\_categories parameter uses the notion of [unicode category](https://en.wikipedia.org/wiki/Unicode_character_property#General_Category) to limit the range of acceptable characters. If you want to see what category a character has you can use Python's unicodedata module to find out: ```pycon >>> from unicodedata import category >>> category('\n') 'Cc' >>> category('\t') 'Cc' >>> category(' ') 'Zs' >>> category('a') 'Ll' ``` The categories we've excluded are *control characters* and *surrogates*. Surrogates are excluded by default but when you explicitly pass in blacklist categories you need to exclude them yourself. So we can put that together with text() to get a name matching our requirements: ```pycon >>> names = text(characters(max_codepoint=1000, exclude_categories=('Cc', 'Cs')), min_size=1) ``` But this is still not quite right: We've allowed spaces in names, but we don't really want a name to start with or end with a space. You can see that this is currently allowed by asking Hypothesis for a more specific example: ```pycon >>> find(names, lambda x: x[0] == ' ') ' ' ``` So let's fix it so that they can't by stripping the spaces off it. To do this we're going to use the strategy's *map* method which lets you compose it with an arbitrary function to post-process the results into the for you want: ```pycon >>> names = text(characters(max_codepoint=1000, exclude_categories=('Cc', 'Cs')), min_size=1).map( ... lambda x: x.strip()) ``` Now let's check that we can no longer have the above problem: ```pycon >>> find(names, lambda x: x[0] == ' ') Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.5/site-packages/hypothesis/core.py", line 648, in find runner.run() File "/usr/lib/python3.5/site-packages/hypothesis/internal/conjecture/engine.py", line 168, in run self._run() File "/usr/lib/python3.5/site-packages/hypothesis/internal/conjecture/engine.py", line 262, in _run self.test_function(data) File "/usr/lib/python3.5/site-packages/hypothesis/internal/conjecture/engine.py", line 68, in test_function self._test_function(data) File "/usr/lib/python3.5/site-packages/hypothesis/core.py", line 616, in template_condition success = condition(result) File "", line 1, in IndexError: string index out of range ``` Whoops! The problem is that our initial test worked because the strings we were generating were always non-empty because of the min\_size parameter. We're still only generating non-empty strings, but if we generate a string which is all spaces then strip it, the result will be empty *after* our map. We can fix this using the strategy's *filter* function, which restricts to only generating things which satisfy some condition: ```pycon >>> names = text(characters(max_codepoint=1000, exclude_categories=('Cc', 'Cs')), min_size=1).map( ... lambda s: s.strip()).filter(lambda s: len(s) > 0) ``` And repeating the check: ```pycon >>> find(names, lambda x: x[0] == ' ') Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.5/site-packages/hypothesis/core.py", line 670, in find raise NoSuchExample(get_pretty_function_description(condition)) hypothesis.errors.NoSuchExample: No examples found of condition lambda x: ``` Hypothesis raises NoSuchExample to indicate that... well, that there's no such example. In general you should be a little bit careful with filter and only use it to filter to conditions that are relatively hard to happen by accident. In this case it's fine because the filter condition only fails if our initial draw was a string consisting entirely of spaces, but if we'd e.g. tried the opposite and tried to filter to strings that *only* had spaces, we'd have had a bad time of it and got a very slow and not very useful test. Anyway, we now really do have a strategy that produces decent names for our projects. Lets put this all together into a test that demonstrates that our names now have the desired properties: ```python from unicodedata import category from hypothesis import given from hypothesis.strategies import characters, text names = ( text(characters(max_codepoint=1000, exclude_categories=("Cc", "Cs")), min_size=1) .map(lambda s: s.strip()) .filter(lambda s: len(s) > 0) ) @given(names) def test_names_match_our_requirements(name): assert len(name) > 0 assert name == name.strip() for c in name: assert 1 <= ord(c) <= 1000 assert category(c) not in ("Cc", "Cs") ``` It's not common practice to write tests for your strategies, but it can be helpful when trying to figure things out. ### Dates and times Hypothesis has date and time generation in a hypothesis.extra subpackage because it relies on pytz to generate them, but other than that it works in exactly the same way as before: ```pycon >>> from hypothesis.extra.datetime import datetimes >>> datetimes().example() datetime.datetime(1642, 1, 23, 2, 34, 28, 148985, tzinfo=) ``` Lets constrain our dates to be UTC, because the sensible thing to do is to use UTC internally and convert on display to the user: ```pycon >>> datetimes(timezones=('UTC',)).example() datetime.datetime(6820, 2, 4, 19, 16, 27, 322062, tzinfo=) ``` We can also constrain our projects to start in a reasonable range of years, as by default Hypothesis will cover the whole of representable history: ```pycon >>> datetimes(timezones=('UTC',), min_year=2000, max_year=2100).example() datetime.datetime(2084, 6, 9, 11, 48, 14, 213208, tzinfo=) ``` Again we can put together a test that checks this behaviour (though we have less code here so it's less useful): ```python from hypothesis import given from hypothesis.extra.datetime import datetimes project_date = datetimes(timezones=("UTC",), min_year=2000, max_year=2100) @given(project_date) def test_dates_are_in_the_right_range(date): assert 2000 <= date.year <= 2100 assert date.tzinfo._tzname == "UTC" ``` ### Putting it all together We can now generate all the parts for our project definitions, but how do we generate a project? The first thing to reach for is the *builds* function. ```pycon >>> from hypothesis.strategies import builds >>> projects = builds(Project, name=names, start=project_date, end=project_date) >>> projects.example() Project 'd!#ñcJν' from 2091-06-22T06:57:39.050162+00:00 to 2057-06-11T02:41:43.889510+00:00 ``` builds lets you take a set of strategies and feed their results as arguments to a function (or, in this case, class. Anything callable really) to create a new strategy that works by drawing those arguments then passing them to the function to give you that example. Unfortunately, this isn't quite right: ```pycon >>> find(projects, lambda x: x.start > x.end) Project '0' from 2000-01-01T00:00:00.000001+00:00 to 2000-01-01T00:00:00+00:00 ``` Projects can start after they end when we use builds this way. One way to fix this would be to use filter(): ```pycon >>> projects = builds(Project, name=names, start=project_date, end=project_date).filter( ... lambda p: p.start < p.end) >>> find(projects, lambda x: x.start > x.end) Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.5/site-packages/hypothesis/core.py", line 670, in find raise NoSuchExample(get_pretty_function_description(condition)) hypothesis.errors.NoSuchExample: No examples found of condition lambda x: ``` This will work, but it starts to edge into the territory of where filter should be avoided - about half of the initially generated examples will fail the filter. What we'll do instead is draw two dates and use whichever one is smallest as the start, and whatever is largest at the end. This is hard to do with builds because of the dependence between the arguments, so instead we'll use builds' more advanced cousin, *composite*: ```python from hypothesis import assume from hypothesis.strategies import composite @composite def projects(draw): name = draw(names) date1 = draw(project_date) date2 = draw(project_date) assume(date1 != date2) start = min(date1, date2) end = max(date1, date2) return Project(name, start, end) ``` The idea of composite is you get passed a magic first argument 'draw' that you can use to get examples out of a strategy. You then make as many draws as you want and use these to return the desired data. You can also use the *assume* function to discard the current call if you get yourself into a state where you can't proceed or where it's easier to start again. In this case we do that when we draw the same data twice. ```pycon >>> projects().example() Project 'rĂ5ĠǓ#' from 2000-05-14T07:21:12.282521+00:00 to 2026-05-12T13:20:43.225796+00:00 >>> find(projects(), lambda x: x.start > x.end) Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.5/site-packages/hypothesis/core.py", line 670, in find raise NoSuchExample(get_pretty_function_description(condition)) hypothesis.errors.NoSuchExample: No examples found of condition lambda x: ``` Note that in all of our examples we're now writing projects() instead of projects. That's because composite returns a function rather than a strategy. Any arguments to your defining function other than the first are also arguments to the one produced by composite. We can now put together one final test that we got this bit right too: ```python @given(projects()) def test_projects_end_after_they_started(project): assert project.start < project.end ``` ### Wrapping up There's a lot more to Hypothesis's data generation than this, but hopefully it gives you a flavour of the sort of things to try and the sort of things that are possible. It's worth having a read of [the documentation](https://hypothesis.readthedocs.io/en/latest/data.html) for this, and if you're still stuck then try asking [the community](https://hypothesis.readthedocs.io/en/latest/community.html) for some help. We're pretty friendly. ================================================ FILE: website/content/2016-05-13-what-is-property-based-testing.md ================================================ --- tags: non-technical, philosophy date: 2016-05-14 09:00 title: What is Property Based Testing? author: drmaciver --- I get asked this a lot, and I write property based testing tools for a living, so you'd think I have a good answer to this, but historically I haven't. This is my attempt to fix that. Historically the definition of property based testing has been "The thing that [QuickCheck](../quickcheck-in-every-language/) does". As a working definition this has served pretty well, but the problem is that it makes us unable to distinguish what the essential features of property-based testing are and what are just [accidental features that appeared in the implementations that we're used to](../referential-transparency/). As the author of a property based testing system which diverges quite a lot from QuickCheck, this troubles me more than it would most people, so I thought I'd set out some of my thoughts on what property based testing is and isn't. This isn't intended to be definitive, and it will evolve over time as my thoughts do, but it should provide a useful grounding for further discussion. There are essentially two ways we can draw a boundary for something like this: We can go narrow or we can go wide. i.e. we can restrict our definitions to things that look exactly like QuickCheck, or things that are in the same general family of behaviour. My inclination is always to go wide, but I'm going to try to rein that in for the purpose of this piece. But I'm still going to start by planting a flag. The following are *not* essential features of property based testing: 1. [Referential Transparency](../referential-transparency/). 2. Types 3. Randomization 4. The use of any particular tool or library As evidence I present the following: 1. Almost every property based testing library, including but not limited to Hypothesis and QuickCheck (both Erlang and Haskell). 2. The many successful property based testing systems for dynamic languages. e.g. Erlang QuickCheck, test.check, Hypothesis. 3. [SmallCheck](https://hackage.haskell.org/package/smallcheck). I have mixed feelings about its effectiveness, but it's unambiguously property-based testing. 4. It's very easy to hand-roll your own testing protocols for property-based testing of a particular result. For example, I've [previously done this for testing a code formatter]( http://www.drmaciver.com/2015/03/27-bugs-in-24-hours/): Run it over a corpus (more on whether running over a corpus "really" counts in a second) of Python files, check whether the resulting formatted code satisfies PEP8. It's classic property-based testing with an oracle. So that provides us with a useful starting point of things that are definitely property based testing. But you're never going to find a good definition by looking at only positive examples, so let's look at some cases where it's more arguable. First off, lets revisit that parenthetical question: Does just testing against a large corpus count? I'm going to go with "probably". I think if we're counting SmallCheck we need to count testing against a large corpus: If you take the first 20k outputs that would be generated by SmallCheck and just replay the test using those the first N of those each time, you're doing exactly the same sort of testing. Similarly if you draw 20k outputs using Hypothesis and then just randomly sample from them each time. I think drawing from a small, fixed, corpus probably *doesn't* count. If you could feasibly write a property based test as 10 example based tests in line in your source code, it's probably really just example based testing. This boundary is a bit, um, fuzzy though. On which note, what about fuzzing? I have previously argued that fuzzing is just a form of property-based testing - you're testing the property "it doesn't crash". I think I've reversed my opinion on this. In particular, I think [the style of testing I advocate for getting started with Hypothesis](../getting-started-with-hypothesis/), *probably* doesn't count as property based testing. I'm unsure about this boundary. The main reason I'm drawing it here is that they do feel like they have a different character - property based testing requires you to reason about how your program should behave, while fuzzing can just be applied to arbitrary programs with minimal understanding of their behaviour - and also that fuzzing feels somehow more fundamental. But you can certainly do property based testing using fuzzing tools, in the same way that you can do it with hand-rolled property based testing systems - I could have taken my Python formatting test above, added [python-afl](http://jwilk.net/software/python-afl) to the mix, and that would still be property based testing. Conversely, you can do fuzzing with property-based testing tools: If fuzzing is not property based testing then not all tests using Hypothesis, QuickCheck, etc. are property based tests. I'm actually OK with that. There's a long tradition of testing tools being used outside their domain - e.g. most test frameworks originally designed as unit testing tools end up getting being used for the whole gamut of testing. So with that in mind, lets provide a definition of fuzzing that I'd like to use: > Fuzzing is feeding a piece of code (function, program, etc.) data from a large corpus, possibly > dynamically generated, possibly dependent on the results of execution on previous data, in > order to see whether it fails. The definitions of "data" and "whether it fails" will vary from fuzzer to fuzzer - some fuzzers will generate only binary data, some fuzzers will generate more structured data. Some fuzzers will look for a process crash, some might just look for a function returning false. (Often definitions of fuzzing focus on "malformed" data. I think this is misguided and fails to consider a lot of things that people would obviously consider fuzzers. e.g. [CSmith](https://embed.cs.utah.edu/csmith/) is certainly a type of fuzzer but deliberately only generates well formed C programs). And given that definition, I think I can now provide a definition of property-based testing: > Property based testing is the construction of tests such that, when these tests are fuzzed, > failures in the test reveal problems with the system under test that could not have been > revealed by direct fuzzing of that system. (If you feel strongly that fuzzing *should* count as property-based testing you can just drop the 'that could not have been etc.' part. I'm on the fence about it myself.) These extra modes of failure then constitute the properties that we're testing. I think this works pretty well at capturing what we're doing with property based testing. It's not perfect, but I'm pretty happy with it. One of the things I particularly like is that it makes it clear that property-based testing is what *you* do, not what the computer does. The part that the computer does is "just fuzzing". Under this point of view, a property-based testing library is really two parts: 1. A fuzzer. 2. A library of tools for making it easy to construct property-based tests using that fuzzer. Hypothesis is very explicitly designed along these lines - the core of it is a structured fuzzing library called Conjecture - which suggests that I may have a bit of bias here, but I still feel that it captures the behaviour of most other property based testing systems quite well, and provides quite a nice middle ground between the wider definition that I wanted and the more tightly focused definition that QuickCheck orients people around. ================================================ FILE: website/content/2016-05-26-exploring-voting-with-hypothesis.md ================================================ --- tags: python, technical, example date: 2016-05-26 11:00 title: Exploring Voting Systems with Hypothesis author: drmaciver --- Hypothesis is, of course, a library for writing tests. But from an *implementation* point of view this is hardly noticeable. Really it's a library for constructing and exploring data and using it to prove or disprove hypotheses about it. It then has a small testing library built on top of it. It's far more widely used as a testing library, and that's really where the focus of its development lies, but with the *find* function you can use it just as well to explore your data interactively. In this article we'll go through an example of doing this, by using it to take a brief look at one of my other favourite subjects: Voting systems. We're going to focus entirely on single winner preferential voting systems: You have a set of candidates, and every voter gives a complete ordering of the candidates from their favourite to their least favourite. The voting system then tries to select a single candidate and declare them the winner. The general Python interface for a voting system we'll use is things that look like the following: ```python def plurality_winner(election): counts = Counter(vote[0] for vote in election) alternatives = candidates_for_election(election) winning_score = max(counts.values()) winners = [c for c, v in counts.items() if v == winning_score] if len(winners) > 1: return None else: return winners[0] ``` That is, they take a list of individual votes, each expressed as a list putting the candidates in order, and return a candidate that is an unambiguous winner or None in the event of a tie. The above implements plurality voting, what most people might think of as "normal voting": The candidate with the most first preference votes wins. The other main voting system we'll consider is Instant Runoff Voting ( which you might know under the name "Alternative Vote" if you follow British politics): ```python def irv_winner(election): candidates = candidates_for_election(election) while len(candidates) > 1: scores = Counter() for vote in election: for c in vote: if c in candidates: scores[c] += 1 break losing_score = min(scores[c] for c in candidates) candidates = [c for c in candidates if scores[c] > losing_score] if not candidates: return None else: return candidates[0] ``` In IRV, we run the vote in multiple rounds until we've eliminated all but one candidate. In each round, we give each candidate a score which is the number of voters who have ranked that candidate highest amongst all the ones remaining. The candidates with the joint lowest score drop out. At the end, we'll either have either zero or one candidates remaining ( we can have zero if all candidates are tied for joint lowest score at some point). If we have zero, that's a draw. If we have one, that's a victory. It seems pretty plausible that these would not produce the same answer all the time (it would be surprising if they did!), but it's maybe not obvious how you would go about constructing an example that shows it. Fortunately, we don't have to because Hypothesis can do it for us! We first create a strategy which generates elections, using Hypothesis's composite decorator: ```python import hypothesis.strategies as st @st.composite def election(draw): candidates = list(range(draw(st.integers(2, 10)))) return draw(st.lists(st.permutations(candidates), min_size=1)) ``` This first draws the set of candidates as a list of integers of size between 2 and 10 (it doesn't really matter what our candidates are as long as they're distinct, so we use integers for simplicity). It then draws an election as lists of permutations of those candidates, as we defined it above. We now write a condition to look for: ```python def differing_without_ties(election): irv = irv_winner(election) if irv is None: return False plurality = plurality_winner(election) if plurality is None: return False return irv != plurality ``` That is, we're interested in elections where neither plurality nor IRV resulted in a tie, but they resulted in distinct candidates winning. We can now run this in the console: ``` >>> from hypothesis import find >>> import voting as v >>> distinct = find(v.election(), v.differing_without_ties) >>> distinct [[0, 1, 2], [0, 1, 2], [1, 0, 2], [2, 1, 0], [0, 1, 2], [0, 1, 2], [1, 0, 2], [1, 0, 2], [2, 1, 0]] ``` The example is a bit large, mostly because we insisted on there being no ties: If we'd broken ties arbitrarily (e.g. preferring the lower numbered candidates) we could have found a smaller one. Also, in some runs Hypothesis ends up finding a slightly smaller election but with four candidates instead of three. We can check to make sure that these really do give different results: ``` >>> v.irv_winner(distinct) 1 >>> v.plurality_winner(distinct) 0 ``` There are a lot of other interesting properties of voting systems to explore, but this is an article about Hypothesis rather than one about voting, so I'll stop here. However the interested reader might want to try to build on this to: 1. Find an election which has a [Condorcet Cycle](https://en.wikipedia.org/wiki/Voting_paradox) 2. Find elections in which the majority prefers the plurality winner to the IRV winner and vice versa. 3. Use @given rather than find and write some tests verifying some of [the classic properties of election systems](https://en.wikipedia.org/wiki/Voting_system#Evaluating_voting_systems_using_criteria). And the reader who isn't that interested in voting systems might still want to think about how this could be useful in other areas: Development is often a constant series of small experiments and, while testing is often a good way to perform them, sometimes you just have a more exploratory "I wonder if...?" question to answer, and it can be extremely helpful to be able to bring Hypothesis to bear there too. ================================================ FILE: website/content/2016-05-29-testing-optimizers-with-hypothesis.md ================================================ --- tags: technical, python, example, properties date: 2016-05-29 21:00 title: Testing Optimizers author: drmaciver redirect_from: /articles/testing-optimizers --- We've [previously looked into testing performance optimizations](../testing-performance-optimizations/) using Hypothesis, but this article is about something quite different: It's about testing code that is designed to optimize a value. That is, you have some function and you want to find arguments to it that maximize (or minimize) its value. As well as being an interesting subject in its own right, this will also nicely illustrate the use of Hypothesis's data() functionality, which allows you to draw more data after the test has started, and will introduce a useful general property that can improve your testing in a much wider variety of settings. We'll use [the Knapsack Packing Problem](https://en.wikipedia.org/wiki/Knapsack_problem) as our example optimizer. We'll use the greedy approximation algorithm described in the link, and see if Hypothesis can show us that it's merely an approximation and not in fact optimal. ```python def pack_knapsack(items, capacity): """Given items as a list [(value, weight)], with value and weight strictly positive integers, try to find a maximum value subset of items with total weight <= capacity""" remaining_capacity = capacity result = [] # Sort in order of decreasing value per unit weight, breaking # ties by taking the lowest weighted items first. items = sorted(items, key=lambda x: (x[1] / x[0], x[1])) for value, weight in items: if weight <= remaining_capacity: result.append((value, weight)) remaining_capacity -= weight return result ``` So how are we going to test this? If we had another optimizer we could test by comparing the two results, but we don't, so we need to figure out properties it should satisfy in the absence of that. The trick we will used to test this is to look for responses to change. That is, we will run the function, we will make a change to the data that should cause the function's output to change in a predictable way, and then we will run the function again and see if it did. But how do we figure out what changes to make? The key idea is that we will look at the output of running the optimizer and use that to guide what changes we make. In particular we will test the following two properties: 1. If we remove an item that was previously chosen as part of the optimal solution, this should not improve the score. 2. If we add an extra copy of an item that was previously chosen as part of the optimal solution, this should not make the score worse. In the first case, any solution that is found when running with one fewer item would also be a possible solution when running with the full set, so if the optimizer is working correctly then it should have found that one if it were an improvement. In the second case, the opposite is true: Any solution that was previously available is still available, so if the optimizer is working correctly it can't find a worse one than it previously found. The two tests look very similar: ```python from hypothesis import Verbosity, assume, given, settings, strategies as st def score_items(items): return sum(value for value, _ in items) PositiveIntegers = st.integers(min_value=1, max_value=10) Items = st.lists(st.tuples(PositiveIntegers, PositiveIntegers), min_size=1) Capacities = PositiveIntegers @given(Items, Capacities, st.data()) def test_cloning_an_item(items, capacity, data): original_solution = pack_knapsack(items, capacity) assume(original_solution) items.append(data.draw(st.sampled_from(original_solution))) new_solution = pack_knapsack(items, capacity) assert score_items(new_solution) >= score_items(original_solution) @given(Items, Capacities, st.data()) def test_removing_an_item(items, capacity, data): original_solution = pack_knapsack(items, capacity) assume(original_solution) item = data.draw(st.sampled_from(original_solution)) items.remove(item) new_solution = pack_knapsack(items, capacity) assert score_items(new_solution) <= score_items(original_solution) ``` (The max_value parameter for integers is inessential but results in nicer example quality). The *data* strategy simply provides an object you can use for drawing more data interactively during the test. This allows us to make our choices dependent on the output of the function when we run it. The draws made will be printed as additional information in the case of a failing example. In fact, both of these tests fail: ``` Falsifying example: test_cloning_an_item(items=[(1, 1), (1, 1), (2, 5)], capacity=7, data=data(...)) Draw 1: (1, 1) ``` In this case what happens is that when Hypothesis clones an item of weight and value 1, the algorithm stuffs its knapsack with all three (1, 1) items, at which point it has spare capacity but no remaining items that are small enough to fit in it. ``` Falsifying example: test_removing_a_chosen_item(items=[(1, 1), (2, 4), (1, 2)], capacity=6, data=data(...)) Draw 1: (1, 1) ``` In this case what happens is the opposite: Previously the greedy algorithm was reaching for the (1, 1) item as the most appealing because it had the highest value to weight ratio, but by including it it only had space for one of the remaining two. When Hypothesis removed that option, it could fit the remaining two items into its knapsack and thus scored a higher point. In this case these failures were more or less expected: As described in the Wikipedia link, for the relatively small knapsacks we're exploring here the greedy approximation algorithm turns out to in fact be quite bad, and Hypothesis can easily expose that. This technique however can be more widely applied: e.g. You can try changing permissions and settings on a user and asserting that they always have more options, or increasing the capacity of a subsystem and seeing that it is always allocated more tasks. ================================================ FILE: website/content/2016-05-31-looking-for-guest-posts.md ================================================ --- tags: non-technical, news date: 2016-05-29 21:00 title: Guest Posts Welcome author: drmaciver --- I would like to see more posters on the hypothesis.works blog. I'm particularly interested in experience reports from people who use Hypothesis in the wild. Could that be you? Details of how to guest post on here: 1. This site is [a Jekyll site on GitHub](https://github.com/HypothesisWorks/HypothesisWorks.github.io). To add a post, create a markdown file in the _posts directory with the appropriate structure and send a pull request. 2. You will want to add an entry for yourself to [the authors data file](https://github.com/HypothesisWorks/HypothesisWorks.github.io/blob/master/_data/authors.yml) 3. You of course retain all copyright to your work. All you're granting is the right to publish it on this site. I'd particularly like to hear from: * People who work in QA * People using the Django support * People using Hypothesis for heavily numerical work * People whose first experience of property based testing was via Hypothesis * People who would like to write about another property based testing system ( and ideally to compare it to Hypothesis) But I'd also like to hear from anyone else who would like to write something about Hypothesis, or property based testing in general: Whether it's an experience report, a cool trick you figured out, an introductory article to Hypothesis, etc. ================================================ FILE: website/content/2016-06-05-incremental-property-based-testing.md ================================================ --- tags: intro, python, technical, properties date: 2016-06-05 16:00 title: Evolving toward property-based testing with Hypothesis author: jml --- Many people are quite comfortable writing ordinary unit tests, but feel a bit confused when they start with property-based testing. This post shows how two ordinary programmers started with normal Python unit tests and nudged them incrementally toward property-based tests, gaining many advantages on the way. Background ---------- I used to work on a command-line tool with an interface much like git's. It had a repository, and within that repository you could create branches and switch between them. Let's call the tool `tlr`. It was supposed to behave something like this: List branches: $ tlr branch foo * master Switch to an existing branch: $ tlr checkout foo * foo master Create a branch and switch to it: $ tlr checkout -b new-branch $ tlr branch foo master * new-branch Early on, my colleague and I found a bug: when you created a new branch with `checkout -b` it wouldn't switch to it. The behavior looked something like this: $ tlr checkout -b new-branch $ tlr branch foo * master new-branch The previously active branch (in this case, `master`) stayed active, rather than switching to the newly-created branch (`new-branch`). Before we fixed the bug, we decided to write a test. I thought this would be a good chance to start using Hypothesis. Writing a simple test --------------------- My colleague was less familiar with Hypothesis than I was, so we started with a plain old Python unit test: ```python def test_checkout_new_branch(self): """Checking out a new branch makes it the current active branch.""" tmpdir = FilePath(self.mktemp()) tmpdir.makedirs() repo = Repository.initialize(tmpdir.path) repo.checkout("new-branch", create=True) self.assertEqual("new-branch", repo.get_active_branch()) ``` The first thing to notice here is that the string `"new-branch"` is not actually relevant to the test. It's just a value we picked to exercise the buggy code. The test should be able to pass with *any valid branch name*. Even before we started to use Hypothesis, we made this more explicit by making the branch name a parameter to the test: ```python def test_checkout_new_branch(self, branch_name="new-branch"): tmpdir = FilePath(self.mktemp()) tmpdir.makedirs() repo = Repository.initialize(tmpdir.path) repo.checkout(branch_name, create=True) self.assertEqual(branch_name, repo.get_active_branch()) ``` (For brevity, I'll elide the docstring from the rest of the code examples) We never manually provided the `branch_name` parameter, but this change made it more clear that the test ought to pass regardless of the branch name. Introducing Hypothesis ---------------------- Once we had a parameter, the next thing was to use Hypothesis to provide the parameter for us. First, we imported Hypothesis: ```python from hypothesis import given, strategies as st ``` And then made the simplest change to our test to actually use it: ```python @given(branch_name=st.just("new-branch")) def test_checkout_new_branch(self, branch_name): tmpdir = FilePath(self.mktemp()) tmpdir.makedirs() repo = Repository.initialize(tmpdir.path) repo.checkout(branch_name, create=True) self.assertEqual(branch_name, repo.get_active_branch()) ``` Here, rather than providing the branch name as a default argument value, we are telling Hypothesis to come up with a branch name for us using the `just("new-branch")` [strategy](https://hypothesis.readthedocs.io/en/latest/data.html). This strategy will always come up with `"new-branch"`, so it's actually no different from what we had before. What we actually wanted to test is that any valid branch name worked. We didn't yet know how to generate any valid branch name, but using a time-honored tradition we pretended that we did: ```python def valid_branch_names(): """Hypothesis strategy to generate arbitrary valid branch names.""" # TODO: Improve this strategy. return st.just("new-branch") @given(branch_name=valid_branch_names()) def test_checkout_new_branch(self, branch_name): tmpdir = FilePath(self.mktemp()) tmpdir.makedirs() repo = Repository.initialize(tmpdir.path) repo.checkout(branch_name, create=True) self.assertEqual(branch_name, repo.get_active_branch()) ``` Even if we had stopped here, this would have been an improvement. Although the Hypothesis version of the test doesn't have any extra power over the vanilla version, it is more explicit about what it's testing, and the `valid_branch_names()` strategy can be reused by future tests, giving us a single point for improving the coverage of many tests at once. Expanding the strategy ---------------------- It's only when we get Hypothesis to start generating our data for us that we really get to take advantage of its bug finding power. The first thing my colleague and I tried was: ```python def valid_branch_names(): return st.text() ``` But that failed pretty hard-core. Turns out branch names were implemented as symlinks on disk, so valid branch name has to be a valid file name on whatever filesystem the tests are running on. This at least rules out empty names, `"."`, `".."`, very long names, names with slashes in them, and probably others (it's actually [really complicated](https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations)). Hypothesis had made something very clear to us: neither my colleague nor I actually knew what a valid branch name should be. None of our interfaces documented it, we had no validators, no clear ideas for rendering & display, nothing. We had just been assuming that people would pick good, normal, sensible names. It was as if we had suddenly gained the benefit of extensive real-world end-user testing, just by calling the right function. This was: 1. Awesome. We've found bugs that our users won't. 2. Annoying. We really didn't want to fix this bug right now. In the end, we compromised and implemented a relatively conservative strategy to simulate the good, normal, sensible branch names that we expected: ```python from string import ascii_lowercase VALID_BRANCH_CHARS = ascii_lowercase + "_-." def valid_branch_names(): # TODO: Handle unicode / weird branch names by rejecting them early, raising nice errors # TODO: How do we handle case-insensitive file systems? return st.text(alphabet=VALID_BRANCH_CHARS, min_size=1, max_size=112) ``` Not ideal, but *much* more extensive than just hard-coding `"new-branch"`, and much clearer communication of intent. Adding edge cases ----------------- There's one valid branch name that this strategy *could* generate, but probably won't: `master`. If we left the test just as it is, then one time in a hojillion the strategy would generate `"master"` and the test would fail. Rather than waiting on chance, we encoded this in the `valid_branch_names` strategy, to make it more likely: ```python def valid_branch_names(): return st.text(alphabet=letters, min_size=1, max_size=112).map( lambda t: t.lower() ) | st.just("master") ``` When we ran the tests now, they failed with an exception due to the branch `master` already existing. To fix this, we used `assume`: ```python from hypothesis import assume @given(branch_name=valid_branch_names()) def test_checkout_new_branch(self, branch_name): assume(branch_name != "master") tmpdir = FilePath(self.mktemp()) tmpdir.makedirs() repo = Repository.initialize(tmpdir.path) repo.checkout(branch_name, create=True) self.assertEqual(branch_name, repo.get_active_branch()) ``` Why did we add `master` to the valid branch names if we were just going to exclude it anyway? Because when other tests say "give me a valid branch name", we want *them* to make the decision about whether `master` is appropriate or not. Any future test author will be compelled to actually think about whether handling `master` is a thing that they want to do. That's one of the great benefits of Hypothesis: it's like having a rigorous design critic in your team. Going forward ------------- We stopped there, but we need not have. Just as the test should have held for any branch, it should also hold for any repository. We were just creating an empty repository because it was convenient for us. If we were to continue, the next step would be to [write a `repositories()` function to generate repositories]({% post_url 2016-05-11-generating-the-right-data/) with more varied contents, commit histories, and existing branches. The test might then look something like this: ```python @given(repo=repositories(), branch_name=valid_branch_names()) def test_checkout_new_branch(self, repo, branch_name): """ Checking out a new branch results in it being the current active branch. """ assume(branch_name not in repo.get_branches()) repo.checkout(branch_name, create=True) self.assertEqual(branch_name, repo.get_active_branch()) ``` This is about as close to a bona fide "property" as you're likely to get in code that isn't a straight-up computer science problem: if you create and switch to a branch that doesn't already exist, the new active branch is the newly created branch. We got there not by sitting down and thinking about the properties of our software in the abstract, nor by necessarily knowing much about property-based testing, but rather by incrementally taking advantage of features of Python and Hypothesis. On the way, we discovered and, umm, contained a whole class of bugs, and we made sure that all future tests would be heaps more powerful. Win. ================================================ FILE: website/content/2016-06-13-testing-configuration-parameters.md ================================================ --- tags: technical, python, properties date: 2016-06-13 00:00 title: Testing Configuration Parameters author: drmaciver --- A lot of applications end up growing a complex configuration system, with a large number of different knobs and dials you can turn to change behaviour. Some of these are just for performance tuning, some change operational concerns, some have other functions. Testing these is tricky. As the number of parameters goes up, the number of possible configuration goes up exponentially. Manual testing of the different combinations quickly becomes completely unmanageable, not to mention extremely tedious. Fortunately, this is somewhere where property-based testing in general and Hypothesis in particular can help a lot. Configuration parameters almost all have one thing in common: For the vast majority of things, they shouldn't change the behaviour. A configuration parameter is rarely going to be a complete reskin of your application. This means that they are relatively easy to test with property-based testing. You take an existing test - either one that is already using Hypothesis or a normal example based test - and you vary some configuration parameters and make sure the test still passes. This turns out to be remarkably effective. Here's an example where I used this technique and found some bugs in the [Argon2]( https://github.com/P-H-C/phc-winner-argon2) password hashing library, using [Hynek](https://hynek.me/)'s [CFFI based bindings](https://github.com/hynek/argon2_cffi). The idea of password hashing is straightforward: Given a password, you can create a hash against which the password can be verified without ever storing the password (after all, you're not storing passwords in plain text on your servers, right?). Although straightforward to describe, there's a lot of difficulty in making a good implementation of this. Argon2 is a fairly recent one which won [the Password Hashing Competition](https://password-hashing.net/) so should be fairly good. We can verify that hashing works correctly fairly immediately using Hypothesis: ```python from argon2 import PasswordHasher from hypothesis import given, strategies as st class TestPasswordHasherWithHypothesis: @given(password=st.text()) def test_a_password_verifies(self, password): ph = PasswordHasher() hash = ph.hash(password) assert ph.verify(hash, password) ``` This takes an arbitrary text password, hashes it and verifies it against the generated hash. This passes. So far, so good. But as you probably expected from its context here, argon2 has quite a lot of different parameters to it. We can expand the test to vary them and see what happens: ```python from argon2 import PasswordHasher from hypothesis import assume, given, strategies as st class TestPasswordHasherWithHypothesis: @given( password=st.text(), time_cost=st.integers(1, 10), parallelism=st.integers(1, 10), memory_cost=st.integers(8, 2048), hash_len=st.integers(12, 1000), salt_len=st.integers(8, 1000), ) def test_a_password_verifies( self, password, time_cost, parallelism, memory_cost, hash_len, salt_len, ): assume(parallelism * 8 <= memory_cost) ph = PasswordHasher( time_cost=time_cost, parallelism=parallelism, memory_cost=memory_cost, hash_len=hash_len, salt_len=salt_len, ) hash = ph.hash(password) assert ph.verify(hash, password) ``` These parameters are mostly intended to vary the difficulty of calculating the hash. Honestly I'm not entirely sure what all of them do. Fortunately for the purposes of writing this test, understanding is optional. In terms of how I chose the specific strategies to get there, I just picked some plausible looking parameters ranges and adjusted them until I wasn't getting validation errors (I did look for documentation, I promise). The assume() call comes from reading the argon2 source to try to find out what the valid range of parallelism was. This ended up finding [two bugs](https://github.com/hynek/argon2_cffi/issues/4), which I duly reported to Hynek, but they actually turned out to be upstream bugs! In both cases, a password would no longer validate against itself: ``` Falsifying example: test_a_password_verifies( password='', time_cost=1, parallelism=1, memory_cost=8, hash_len=4, salt_len=8, ) ``` ``` Falsifying example: test_a_password_verifies( password='', time_cost=1, parallelism=1, memory_cost=8, hash_len=513, salt_len=8 ) ``` (I found the second one by manually determining that the first bug happened whenever salt_len < 12 and manually ruling that case out). One interesting thing about both of these bugs is that they're actually not bugs in the Python library but are both downstream bugs. I hadn't set out to do that when I wrote these tests, but it nicely validates that Hypothesis is rather useful for testing C libraries as well as Python, given how easy they are to bind to with CFFI. ================================================ FILE: website/content/2016-06-30-tests-as-complete-specifications.md ================================================ --- tags: technical, python, properties, intro date: 2016-06-30 00:00 title: Testing as a Complete Specification author: drmaciver --- Sometimes you're lucky enough to have problems where the result is completely specified by a few simple properties. This doesn't necessarily correspond to them being easy! Many such problems are actually extremely fiddly to implement. It does mean that they're easy to *test* though. Lets see how. Let's look at the problem of doing a binary search. Specifically we'll look at a left biased binary search: Given a sorted list and some value, we want to find the smallest index that we can insert that value at and still have the result be sorted. So we've got the following properties: 1. binary_search must always return a valid index to insert the value at. 2. If we insert the value at that index the result must be sorted. 3. If we insert the value at any *smaller* index, the result must *not* be sorted. Using Hypothesis we can write down tests for all these properties: ```python from hypothesis import given, strategies as st @given(lists(integers()).map(sorted), integers()) def test_binary_search_gives_valid_index(ls, v): i = binary_search(ls, v) assert 0 <= i <= len(ls) @given(lists(integers()).map(sorted), integers()) def test_inserting_at_binary_search_remains_sorted(ls, v): i = binary_search(ls, v) ls.insert(i, v) assert sorted(ls) == ls @given(lists(integers()).map(sorted), integers()) def test_inserting_at_smaller_index_gives_unsorted(ls, v): for i in range(binary_search(ls, v)): ls2 = list(ls) ls2.insert(i, v) assert sorted(ls2) != ls ``` If these tests pass, our implementation must be perfectly correct, right? They capture the specification of the binary_search function exactly, so they should be enough. And they mostly are, but they suffer from one problem that will sometimes crop up with property-based testing: They don't hit all bugs with quite high enough probability. This is the difference between testing and mathematical proof: A proof will guarantee that these properties *always* hold, while a test can only guarantee that they hold in the areas that it's checked. A test using Hypothesis will check a much wider area than most hand-written tests, but it's still limited to a finite set of examples. Lets see how this can cause us problems. Consider the following implementation of binary search: ```python def binary_search(list, value): if not list: return 0 if value > list[-1]: return len(list) if value <= list[0]: return 0 lo = 0 hi = len(list) - 1 while lo + 1 < hi: mid = (lo + hi) // 2 pivot = list[mid] if value < pivot: hi = mid elif value == pivot: return mid else: lo = mid return hi ``` This implements the common check that if our pivot index ever has exactly the right value we return early there. Unfortunately in this case that check is wrong: It violates the property that we should always find the *smallest* property, so the third test should fail. And sure enough, if you run the test enough times it eventually *does* fail: ``` Falsifying example: test_inserting_at_smaller_index_gives_unsorted( ls=[0, 1, 1, 1, 1], v=1 ) ``` (you may also get (ls=[-1, 0, 0, 0, 0], v=0)) However when I run it it usually *doesn't* fail the first time. It usually takes somewhere between two and five runs before it fails. This is because in order to trigger this behaviour being wrong you need quite specific behaviour: value needs to appear in ls at least twice, and it needs to do so in such a way that one of the indices where it appears that is *not* the first one gets chosen as mid at some point in the process. Hypothesis does some things that boost the chances of this happening, but they don't boost it *that* much. Of course, once it starts failing Hypothesis's test database kicks in, and the test keeps failing until the bug is fixed, but low probability failures are still annoying because they move the point at which you discover the problem further away from when you introduced it. This is especially true when you're using [stateful testing ](../rule-based-stateful-testing/), because the search space is so large that there are a lot of low probability bugs. Fortunately there's an easy fix for this case: You can write additional tests that are more likely to discover bugs because they are less sensitively dependent on the example chosen by Hypothesis to exhibit interesting behaviours. Consider the following test: ```python @given(lists(integers()).map(sorted), integers()) def test_inserting_at_result_point_and_searching_again(ls, v): i = binary_search(ls, v) ls.insert(i, v) assert binary_search(ls, v) == i ``` The idea here is that by doing a search, inserting the value at that index, and searching again we cannot have moved the insert point: Inserting there again would still result in a sorted list, and inserting any earlier would still have resulted in an unsorted list, so this must still be the same insert point (this should remind you a bit of [the approach for testing optimizers we used before]( ../testing-optimizers-with-hypothesis/) ). This test fails pretty consistently because it doesn't rely nearly so much on finding duplicates: Instead it deliberately creates them in a place where they are likely to be problematic. So, in conclusion: 1. When the problem is fully specified, this gives you a natural source of tests that you can easily write using Hypothesis. 2. However this is where your tests should *start* rather than finish, and you still need to think about other interesting ways to test your software. ================================================ FILE: website/content/2016-07-04-calculating-the-mean.md ================================================ --- tags: technical, python, properties, intro date: 2016-07-04 00:00 title: Calculating the mean of a list of numbers author: drmaciver --- Consider the following problem: You have a list of floating point numbers. No nasty tricks - these aren't NaN or Infinity, just normal "simple" floating point numbers. Now: Calculate the mean (average). Can you do it? It turns out this is a hard problem. It's hard to get it even *close* to right. Lets see why. Consider the following test case using Hypothesis: ```python from hypothesis import given from hypothesis.strategies import floats, lists @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1)) def test_mean_is_within_reasonable_bounds(ls): assert min(ls) <= mean(ls) <= max(ls) ``` This isn't testing much about correctness, only that the value of the mean is within reasonable bounds for the list: There are a lot of functions that would satisfy this without being the mean. min and max both satisfy this, as does the median, etc. However, almost nobody's implementation of the mean satisfies this. To see why, lets write our own mean: ```python def mean(ls): return sum(ls) / len(ls) ``` This seems reasonable enough - it's just the definition of the mean - but it's wrong: ``` assert inf <= 8.98846567431158e+307 + where inf = mean([8.988465674311579e+307, 8.98846567431158e+307]) + and 8.98846567431158e+307 = max([8.988465674311579e+307, 8.98846567431158e+307]) Falsifying example: test_mean_is_within_reasonable_bounds( ls=[8.988465674311579e+307, 8.98846567431158e+307] ) ``` The problem is that finite floating point numbers may be large enough that their sum overflows to infinity. When you then divide infinity by a finite number you still get infinity, which is out of the range. So to prevent that overflow, lets try to bound the size of our numbers by the length *first*: ```python def mean(ls): return sum(l / len(ls) for l in ls) ``` ``` assert min(ls) <= mean(ls) <= max(ls) assert 1.390671161567e-309 <= 1.390671161566996e-309 where 1.390671161567e-309 = min([1.390671161567e-309, 1.390671161567e-309, 1.390671161567e-309]) and 1.390671161566996e-309 = mean([1.390671161567e-309, 1.390671161567e-309, 1.390671161567e-309]) Falsifying example: test_mean_is_within_reasonable_bounds( ls=[1.390671161567e-309, 1.390671161567e-309, 1.390671161567e-309] ) ``` In this case the problem you run into is not overflow, but the lack of precision of floating point numbers: Floating point numbers are only exact up to powers of two times an integer, so dividing by three will cause rounding errors. In this case we have the problem that (x / 3) * 3 may not be equal to x in general. So now we've got a sense of why this might be hard. Lets see how existing implementations do at satisfying this test. First let's try numpy: ```python import numpy as np def mean(ls): return np.array(ls).mean() ``` This runs into the problem we had in our first implementation: ``` assert min(ls) <= mean(ls) <= max(ls) assert inf <= 8.98846567431158e+307 where inf = mean([8.988465674311579e+307, 8.98846567431158e+307]) and 8.98846567431158e+307 = max([8.988465674311579e+307, 8.98846567431158e+307]) Falsifying example: test_mean_is_within_reasonable_bounds( ls=[8.988465674311579e+307, 8.98846567431158e+307] ) ``` There's also the new statistics module from Python 3.4. Unfortunately, this is broken too ([this is fixed in 3.5.2](https://bugs.python.org/issue25177)): ``` OverflowError: integer division result too large for a float Falsifying example: test_mean_is_within_reasonable_bounds( ls=[8.988465674311579e+307, 8.98846567431158e+307] ) ``` In the case where we previously overflowed to infinity this instead raises an error. The reason for this is that internally the statistics module is converting everything to the Fraction type, which is an arbitrary precision rational type. Because of the details of where and when they were converting back to floats, this produced a rational that couldn't be readily converted back to a float. It's relatively easy to write an implementation which passes this test by simply cheating and not actually calculating the mean: ```python def clamp(lo, v, hi): return min(hi, max(lo, v)) def mean(ls): return clamp(min(ls), sum(ls) / len(ls), max(ls)) ``` i.e. just restricting the value to lie in the desired range. However getting an actually correct implementation of the mean (which *would* pass this test) is quite hard: To see just how hard, here's a [30 page paper on calculating the mean of two numbers](https://hal.archives-ouvertes.fr/file/index/docid/576641/filename/computing-midpoint.pdf). I wouldn't feel obliged to read that paper if I were you. I *have* read it and I don't remember many of the details. This test is a nice instance of a general one: Once you've got the [this code doesn't crash](../getting-started-with-hypothesis/), tests working, you can start to layer on additional constraints on the result value. As this example shows, even when the constraints you impose are *very* lax it can often catch interesting bugs. It also demonstrates a problem: Floating point mathematics is *very* hard, and this makes it somewhat unsuitable for testing with Hypothesis. This isn't because Hypothesis is *bad* at testing floating point code, it's because it's good at showing you how hard programming actually is, and floating point code is much harder than people like to admit. As a result, you probably don't care about the bugs it will find: Generally speaking most peoples' attitude to floating point errors is "Eh, those are weird numbers, we don't really care about that. It's probably good enough". Very few people are actually prepared to do the required work of a numerical sensitivity analysis that is needed if you want your floating point code to be correct. I used to use this example a lot for demonstrating Hypothesis to people, but because of these problems I tend not to any more: Telling people about bugs they're not going to want to fix will get you neither bug fixes nor friends. But it's worth knowing that this is a problem: Programming *is* really hard, and ignoring the problems won't make it less hard. You can ignore the correctness issues until they actually bite you, but it's best not to be surprised when they do. And it's also worth remembering the general technique here, because this isn't just useful for floating point numbers: Most code can benefit from this, and most of the time the bugs it tells you won't be nearly this unpleasant. ================================================ FILE: website/content/2016-07-09-hypothesis-3.4.1-release.md ================================================ --- tags: news, python, non-technical date: 2016-07-09 00:00 title: Hypothesis for Python 3.4.1 Release author: drmaciver --- This is a bug fix release for a single bug: * On Windows when running two Hypothesis processes in parallel (e.g. using pytest-xdist) they could race with each other and one would raise an exception due to the non-atomic nature of file renaming on Windows and the fact that you can’t rename over an existing file. This is now fixed. ## Notes My tendency of doing immediate patch releases for bugs is unusual but generally seems to be appreciated. In this case this was a bug that was blocking [a py.test merge](https://github.com/pytest-dev/pytest/pull/1705). I suspect this is not the last bug around atomic file creation on Windows. Cross platform atomic file creation seems to be a harder problem than I would have expected. ================================================ FILE: website/content/2016-07-13-hypothesis-3.4.2-release.md ================================================ --- tags: news, python, non-technical date: 2016-07-13 00:00 title: 3.4.2 Release of Hypothesis for Python author: drmaciver --- This is a bug fix release, fixing a number of problems with the settings system: * Test functions defined using @given can now be called from other threads (Issue #337) * Attempting to delete a settings property would previously have silently done the wrong thing. Now it raises an AttributeError. * Creating a settings object with a custom database_file parameter was silently getting ignored and the default was being used instead. Now it’s not. ## Notes For historic reasons, _settings.py had been excluded from the requirement to have 100% branch coverage. Issue #337 would have been caught by a coverage requirement: the code in question simply couldn't have worked, but it was not covered by any tests, so it slipped through. As part of the general principle that bugs shouldn't just be fixed without addressing the reason why the bug slipped through in the first place, I decided to impose the coverage requirements on _settings.py as well, which is how the other two bugs were found. Both of these had code that was never run during tests - in the case of the deletion bug there was a \_\_delete\_\_ descriptor method that was never being run, and in the case of the database\_file one there was a check later that could never fire because the internal \_database field was always being set in \_\_init\_\_. I feel like this experiment thoroughly validated that 100% coverage is a useful thing to aim for. Unfortunately it also pointed out that the settings system is *much* more complicated than it needs to be. I'm unsure what to do about that - some of its functionality is a bit too baked into the public API to lightly change, and I'm don't think it's worth breaking that just to simplify the code. ================================================ FILE: website/content/2016-07-23-what-is-hypothesis.md ================================================ --- tags: python, intro date: 2016-07-24 00:00 title: What is Hypothesis? author: drmaciver --- Hypothesis is a library designed to help you write what are called *property-based tests*. The key idea of property based testing is that rather than writing a test that tests just a single scenario, you write tests that describe a range of scenarios and then let the computer explore the possibilities for you rather than having to hand-write every one yourself. In order to contrast this with the sort of tests you might be used to, when talking about property-based testing we tend to describe the normal sort of testing as *example-based testing*. Property-based testing can be significantly more powerful than example based testing, because it automates the most time consuming part of writing tests - coming up with the specific examples - and will usually perform it better than a human would. This allows you to focus on the parts that humans are better at - understanding the system, its range of acceptable behaviours, and how they might break. You don't *need* a library to do property-based testing. If you've ever written a test which generates some random data and uses it for testing, that's a property-based test. But having a library can help you a lot, making your tests easier to write, more robust, and better at finding bugs. In the rest of this article we'll see how. ### How to use it The key object of Hypothesis is a *strategy*. A strategy is a recipe for describing the sort of data you want to generate. The existence of a rich and comprehensive strategy library is the first big advantage of Hypothesis over a more manual process: Rather than having to hand-write generators for the data you want, you can just compose the ones that Hypothesis provides you with to get the data you want. e.g. if you want a lists of floats, you just use the strategy lists(floats()). As well as being easier to write, the resulting data will usually have a distribution that is much better at finding edge cases than all but the most heavily tuned manual implementations. As well as the basic out of the box strategy implementations, Hypothesis has a number of tools for composing strategies with user defined functions and constraints, making it fairly easy to generate the data you want. Note: For the remainder of this article I'll focus on the Hypothesis for Python implementation. The Java implementation is similar, but has a number of small differences that I'll discuss in a later article. Once you know how to generate your data, the main entry point to Hypothesis is the @given decorator. This takes a function that accepts some arguments and turns it into a normal test function. An important consequence of that is that Hypothesis is not itself a test runner. It works inside your normal testing framework - it will work fine with nose, py.test, unittest, etc. because all it does is expose a function of the right name that the test runner can then pick up. Using it with a py.test or nose style test looks like this: ```python from mercurial.encoding import fromutf8b, toutf8b from hypothesis import given from hypothesis.strategies import binary @given(binary()) def test_decode_inverts_encode(s): assert fromutf8b(toutf8b(s)) == s ``` (This is an example from testing Mercurial which found two bugs: [4927](https://bz.mercurial-scm.org/show_bug.cgi?id=4927) and [5031](https://bz.mercurial-scm.org/show_bug.cgi?id=5031)). In this test we are asserting that for any binary string, converting it to its utf8b representation and back again should result in the same string we started with. The @given decorator then handles executing this test over a range of different binary strings without us having to explicitly specify any of the examples ourself. When this is first run, you will see an error that looks something like this: ``` Falsifying example: test_decode_inverts_encode(s='\xc2\xc2\x80') Traceback (most recent call last): File "/home/david/.pyenv/versions/2.7/lib/python2.7/site-packages/hypothesis/core.py", line 443, in evaluate_test_data search_strategy, test, File "/home/david/.pyenv/versions/2.7/lib/python2.7/site-packages/hypothesis/executors.py", line 58, in default_new_style_executor return function(data) File "/home/david/.pyenv/versions/2.7/lib/python2.7/site-packages/hypothesis/core.py", line 110, in run return test(*args, **kwargs) File "/home/david/hg/test_enc.py", line 8, in test_decode_inverts_encode assert fromutf8b(toutf8b(s)) == s File "/home/david/hg/mercurial/encoding.py", line 485, in fromutf8b u = s.decode("utf-8") File "/home/david/.pyenv/versions/2.7/lib/python2.7/encodings/utf_8.py", line 16, in decode return codecs.utf_8_decode(input, errors, True) UnicodeDecodeError: 'utf8' codec can't decode byte 0xc2 in position 1: invalid continuation byte ``` Note that the falsifying example is quite small. Hypothesis has a "simplification" process which runs behind the scenes and generally tries to give the impression that the test simply failed with one example that happened to be a really nice one. Another important thing to note is that because of the random nature of Hypothesis and because this bug is relatively hard to find, this test may run successfully a couple of times before finding it. However, once that happens, when we rerun the test it will keep failing with the same example. This is because Hypothesis has a local test database that it saves failing examples in. When you rerun the test, it will first try the previous failure. This is pretty important: It means that although Hypothesis is at its heart random testing, it is *repeatable* random testing: A bug will never go away by chance, because a test will only start passing if the example that previously failed no longer failed. (This isn't entirely true because a bug could be caused by random factors such as timing or hash randomization. However in these cases it's true for example-based testing as well. If anything Hypothesis is *more* robust here because it will tend to find these cases with higher probability). Ultimately that's "all" Hypothesis does: It provides repeatability, reporting and simplification for randomized tests, and it provides a large library of generators to make it easier to write them. Because of these features, the workflow is a huge improvement on writing your own property-based tests by hand, and thanks to the library of generators it's often even easier than writing your own example based tests by hand. ### What now? If you want to read more on the subject, there are a couple places you could go: * If you want to know more of the details of the process I described when a test executes, you can check out the [Anatomy of a test](../anatomy-of-a-test/) article which will walk you through the steps in more detail. * If you'd like more examples of how to use it, check out the rest of the [articles](/articles). But really the best way to learn more is to try to use it! As you've hopefully seen in this article, it's quite approachable to get started with. Try writing some tests and see what happens. ================================================ FILE: website/content/2016-08-09-hypothesis-pytest-fixtures.md ================================================ --- tags: technical, faq, python date: 2016-08-09 10:00 title: How do I use pytest fixtures with Hypothesis? author: drmaciver --- [pytest](http://doc.pytest.org/en/latest/) is a great test runner, and is the one Hypothesis itself uses for testing (though Hypothesis works fine with other test runners too). It has a fairly elaborate [fixture system](http://doc.pytest.org/en/latest/fixture.html), and people are often unsure how that interacts with Hypothesis. In this article we'll go over the details of how to use the two together. Mostly, Hypothesis and py.test fixtures don't interact: Each just ignores the other's presence. When using a @given decorator, any arguments that are not provided in the @given will be left visible in the final function: ```python from inspect import signature from hypothesis import given, strategies as st @given(a=st.none(), c=st.none()) def test_stuff(a, b, c, d): pass print(signature(test_stuff)) ``` This then outputs the following: ``` ``` We've hidden the arguments 'a' and 'c', but the unspecified arguments 'b' and 'd' are still left to be passed in. In particular, they can be provided as py.test fixtures: ```python from pytest import fixture from hypothesis import given, strategies as st @fixture def stuff(): return "kittens" @given(a=st.none()) def test_stuff(a, stuff): assert a is None assert stuff == "kittens" ``` This also works if we want to use @given with positional arguments: ```python from pytest import fixture from hypothesis import given, strategies as st @fixture def stuff(): return "kittens" @given(t.none()) def test_stuff(stuff, a): assert a is None assert stuff == "kittens" ``` The positional argument fills in from the right, replacing the 'a' argument and leaving us with 'stuff' to be provided by the fixture. Personally I don't usually do this because I find it gets a bit confusing - if I'm going to use fixtures then I always use the named variant of given. There's no reason you *can't* do it this way if you prefer though. @given also works fine in combination with parametrized tests: ```python import pytest from hypothesis import given, strategies as st @pytest.mark.parametrize("stuff", [1, 2, 3]) @given(a=st.none()) def test_stuff(a, stuff): assert a is None assert 1 <= stuff <= 3 ``` This will run 3 tests, one for each value for 'stuff'. There is one unfortunate feature of how this interaction works though: In pytest you can declare fixtures which do set up and tear down per function. These will "work" with Hypothesis, but they will run once for the entire test function rather than once for each time given calls your test function. So the following will fail: ```python from pytest import fixture from hypothesis import given, strategies as st counter = 0 @fixture(scope="function") def stuff(): global counter counter = 0 @given(a=st.none()) def test_stuff(a, stuff): global counter counter += 1 assert counter == 1 ``` The counter will not get reset at the beginning of each call to the test function, so it will be incremented each time and the test will start failing after the first call. There currently aren't any great ways around this unfortunately. The best you can really do is do manual setup and teardown yourself in your tests using Hypothesis (e.g. by implementing a version of your fixture as a context manager). Long-term, I'd like to resolve this by providing a mechanism for allowing fixtures to be run for each example (it's probably not correct to have *every* function scoped fixture run for each example), but for now it's stalled because it [requires changes on the py.test side as well as the Hypothesis side](https://github.com/pytest-dev/pytest/issues/916) and we haven't quite managed to find the time and place to collaborate on figuring out how to fix this yet. ================================================ FILE: website/content/2016-08-19-recursive-data.md ================================================ --- tags: technical, python, intro date: 2016-08-19 10:00 title: Generating recursive data author: drmaciver --- Sometimes you want to generate data which is *recursive*. That is, in order to draw some data you may need to draw some more data from the same strategy. For example we might want to generate a tree structure, or arbitrary JSON. Hypothesis has the *recursive* function in the hypothesis.strategies module to make this easier to do. This is an article about how to use it. Lets start with a simple example of drawing tree shaped data: In our example a tree is either a single boolean value (a leaf node), or a tuple of two child trees. So a tree might be e.g. True, or (False, True), or ((True, True), False), etc. First off, it might not be obvious that you *need* the recursive strategy. In principle you could just do this with composite: ```python import hypothesis.strategies as st @st.composite def composite_tree(draw): return draw( st.one_of( st.booleans(), st.tuples(composite_tree(), composite_tree()), ) ) ``` If you try drawing examples from this you'll probably see one of three scenarios: 1. You'll get a single boolean value 2. You'll get a very large tree 3. You'll get a RecursionError from a stack overflow It's unlikely that you'll see any non-trivial small examples. The reason for this is that this sort of recursion tends to explode in size: If this were implemneted as a naive random generation process then the expected size of the tree would be infinite. Hypothesis has some built in limiters to stop it ever trying to actually generate infinitely large amounts of data, but it will still tend to draw trees that are very large if they're not trivial, and it can't do anything about the recursion problem. So instead of using this sort of unstructured recursion, Hypothesis exposes a way of doing recursion in a slightly more structured way that lets it control the size of the generated data much more effectively. This is the recursive strategy. In order to use the recursive strategy you need two parts: 1. A base strategy for generating "simple" instances of the data that you want. 2. A function that takes a child strategy that generates data of the type you want and returns a new strategy generating "larger" instances. So for example for our trees of booleans and tuples we could use booleans() for the first and something for returning tuples of children for the second: ```python recursive_tree = st.recursive( st.booleans(), lambda children: st.tuples(children, children) ) ``` The way to think about the recursive strategy is that you're repeatedly building up a series of strategies as follows: ```python s1 = base s2 = one_of(s1, extend(s1)) s3 = one_of(s2, extend(s2)) ... ``` So at each level you augment the things from the previous level with your extend function. Drawing from the resulting recursive strategy then picks one of this infinite sequence of strategies and draws from it (this isn't quite what happens in practice, but it's pretty close). The resulting strategy does a much better job of drawing small and medium sized trees than our original composite based one does, and should never raise a RecursionError: ``` >>> recursive_tree.example() ((False, True), ((True, True), False)) >>> recursive_tree.example() ((((False, False), True), False), False) >>> recursive_tree.example() (False, True) >>> recursive_tree.example() True ``` You can also control the size of the trees it draws with the third parameter to recursive: ``` >>> st.recursive(st.booleans(), lambda children: st.tuples(children, children), max_leaves=2).example() True >>> st.recursive(st.booleans(), lambda children: st.tuples(children, children), max_leaves=2).example() (True, False) ``` The max_leaves parameter controls the number of values drawn from the 'base' strategy. It defaults to 50, which will tend to give you moderately sized values. This helps keep example sizes under control, as otherwise it can be easy to create extend functions which cause the size to grow very rapidly. In this particular example, Hypothesis will typically not hit the default, but consider something like the following: ``` >>> st.recursive(st.booleans(), lambda children: st.lists(children, min_size=3)).example() [[False, True, False, False, False, True, True, True, False, False, False, True, True, False], False, [False, True, False, True, False], [True, False, True, False, False, False]] ``` In this case the size of the example will tend to push up against the max_leaves value because extend() grows the strategy in size quite rapidly, so if you want larger examples you will need to turn up max_leaves. ================================================ FILE: website/content/2016-08-31-how-many-tests.md ================================================ --- tags: technical, details, faq, python date: 2016-08-31 00:00 title: How many times will Hypothesis run my test? author: drmaciver --- This is one of the most common first questions about Hypothesis. People generally assume that the number of tests run will depend on the specific strategies used, but that's generally not the case. Instead Hypothesis has a fairly fixed set of heuristics to determine how many times to run, which are mostly independent of the data being generated. But how many runs is *that*? The short answer is 200. Assuming you have a default configuration and everything is running smoothly, Hypothesis will run your test 200 times. The longer answer is "It's complicated". It will depend on the exact behaviour of your tests and the value of some settings. In this article I'll try to clear up some of the specifics of which [settings](http://hypothesis.readthedocs.io/en/latest/settings.html) affect the answer and how. Advance warning: This is a set of heuristics built up over time. It's probably not the best choice of heuristics, but it mostly seems to work well in practice. It will hopefully be replaced with a simpler set of rules at some point. The first setting that affects how many times the test function will be called is the timeout setting. This specifies a maximum amount of time for Hypothesis to run your tests for. Once that has exceeded it will stop and not run any more (note: This is a soft limit, so it won't interrupt a test midway through). The result of this is that slow tests may get run fewer times. By default the timeout is one minute, which is high enough that most tests shouldn't hit it, if your tests take somewhere in the region of 300-400ms on average they will start to hit the timeout. The timeout is respected regardless of whether the test passes or fails, but other than that the behaviour for a passing test is very different from a failing one. ### Passing tests For the passing case there are two other settings that affect the answer: max\_examples and max\_iterations. In the normal case, max\_examples is what you can think of as the number of test runs. The difference comes when you start using assume or filter (and a few other cases). Hypothesis distinguishes between a *valid* test run and an invalid one - if assume has been called with a falsey value or at some point in the generation process it got stuck (e.g. because filter couldn't find any satisfying examples) it aborts the example and starts again from the beginning. max\_examples counts only valid examples while max\_iterations counts all examples, valid or otherwise. Some duplicate tests will also be considered invalid (though Hypothesis can't distinguish all duplicates. e.g. if you did integers().map(lambda x: 1) it would think you had many distinct values when you only had one). The default value for max_iterations is currently 1000. To see why it's important to have the max\_iterations limit, consider something like: ```python from hypothesis import assume, given, strategies as st @given(st.integers()) def test_stuff(i): assume(False) ``` Then without a limit on invalid examples this would run forever. Conversely however, treating valid examples specially is useful because otherwise even casual use of assume would reduce the number of tests you run, reducing the quality of your testing. Another thing to note here is that the test with assume(False) will actually fail, raising: ``` hypothesis.errors.Unsatisfiable: Unable to satisfy assumptions of hypothesis test_stuff. Only 0 examples considered satisfied assumptions ``` This is because of the min\_satisfying\_examples setting: If Hypothesis couldn't find enough valid test cases then it will fail the test rather than silently doing the wrong thing. min\_satisfying\_examples will never increase the number of tests run, only fail the test if that number of valid examples haven't been run. If you're hitting this failure you can either turn it down or turn the timeout or max\_iterations up. Better, you can figure out *why* you're hitting that and fix it, because it's probably a sign you're not getting much benefit out of Hypothesis. ### Failing tests If in the course of normal execution Hypothesis finds an example which causes your test to fail, it switches into shrinking mode. Shrinking mode tries to take your example and produce a smaller one. It ignores max\_examples and max\_iterations but respects timeout. It also respects one additional setting: max\_shrinks. max\_shrinks is the maximum number of *failing* tests that Hypothesis will see before it stops. It may try any number of valid or invalid examples in the course of shrinking. This is because failing examples tend to be a lot rarer than passing or invalid examples, so it makes more sense to limit based on that if we want to get good examples out at the end. Once Hypothesis has finished shrinking it will run your test once more to replay it for display: In the final run only it will print out the example and any notes, and will let the exception bubble up to the test runner to be handled as normal. ### In Conclusion "It's complicated". These heuristics are probably not the best. They've evolved over time, and are definitely not the ones that you or I would obviously come up with if you sat down and designed the system from scratch. Fortunately, you're not expected to know these heuristics by heart and mostly shouldn't have to. I'm working on a new feature that will help show how many examples Hypothesis has tried and help debug why it's stopped at that point. Hopefully it will be coming in a release in the near future. ================================================ FILE: website/content/2016-09-04-hypothesis-vs-eris.md ================================================ --- tags: technical, python, intro date: 2016-09-04 15:00 title: Hypothesis vs. Eris author: giorgiosironi --- [Eris](https://github.com/giorgiosironi/eris/) is a library for property-based testing of PHP code, inspired by the mature frameworks that other languages provide like [QuickCheck](https://hackage.haskell.org/package/QuickCheck), Clojure's [test.check](https://github.com/clojure/test.check) and of course Hypothesis. Here is a side-by-side comparison of some basic and advanced features that have been implemented in both Hypothesis and Eris, which may help developers coming from either Python or PHP and looking at the other side. ## Hello, world The first example can be considered an `Hello, world` of randomized testing: given two random numbers, their sum should be constant no matter the order in which they are summed. The test case has two input parameters, so that it can be run dozens or hundreds of times, each run with different and increasingly complex input values. Hypothesis provides an idiomatic Python solution, the `@given` decorator, that can be used to compose the strategies objects instantiated from `hypothesis.strategies`. The test case is very similar to an example-based one, except for the addition of arguments: ```python import unittest from hypothesis import given, strategies as st class TestEris(unittest.TestCase): "Comparing the syntax and implementation of features with Eris" @given(st.integers(), st.integers()) def test_sum_is_commutative(self, first, second): x = first + second y = second + first self.assertEqual( x, y, "Sum between %d and %d should be commutative" % (first, second) ) ``` In this example, I am using the unittest syntax for maximum test portability, but this does not affect Hypothesis, which works across testing frameworks. Eris provides functionality with a trait instead, that can be composed into the test cases that need access to randomized input. The test method is not augmented with additional parameters, but its code is moved inside an anonymous function for the `then()` primitive. Input distributions are defined in a mathematically-named `forAll()` primitive: ```php forAll( Generator\int(), Generator\int() ) ->then(function ($first, $second) { $x = $first + $second; $y = $second + $first; $this->assertEquals( $x, $y, "Sum between {$first} and {$second} should be commutative" ); }); } ``` Both these tests will be run hundreds of times, each computing two sums and comparing them for equality with the assertion library of the underlying testing framework (unittest and PHPUnit). ## Composing strategies A very simple composition problem consists of generating a collection data structured whose elements are drawn from a known distribution, for example a list of integers. Hypothesis provides *strategies* that can compose other strategies, in this case to build a list of random integers of arbitrary length: ```python @given(st.lists(st.integers())) def test_sorting_a_list_twice_is_the_same_as_sorting_it_once(self, xs): xs.sort() ys = list(xs) xs.sort() self.assertEqual(xs, ys) ``` Eris calls *generators* the objects representing statistical distributions, but uses the same compositional pattern for higher-order values like lists and all collections: ```php public function testArraySortingIsIdempotent() { $this ->forAll( Generator\seq(Generator\nat()) ) ->then(function ($array) { sort($array); $expected = $array; sort($array); $this->assertEquals($expected, $array); }); } ``` In both these test cases, we generate a random list and check that the sorting operation is idempotent: operating it twice on the input data gives the same result as operating it once. ## Filtering generated values Not all problems are abstract enough to accept all values in input, so it may be necessary to exclude part of the generated values when they do not fit our needs. Hypothesis provides a `filter()` method to apply a lambda to values, expressing a condition for them to be included in the test: ```python @given(st.integers().filter(lambda x: x > 42)) def test_filtering(self, x): self.assertGreater(x, 42) ``` Eris allows to filter values with a predicate in the same way, but prefers to allocate the filter to the generic `ForAll` object rather than decorate it on each Generator: ```php public function testWhenWithAnAnonymousFunctionWithGherkinSyntax() { $this ->forAll( Generator\choose(0, 1000) ) ->when(function ($n) { return $n > 42; }) ->then(function ($number) { $this->assertTrue( $number > 42, "\$number was filtered to be more than 42, but it's $number" ); }); } ``` Filtering and the constructs that follow are integrated with shrinking in both Hypothesis and Eris: once a test fails and shrinking starts, constraints and transformations will continue to be applied to the generated values so that the simpler inputs are still going to belong to the same distribution; in this scenario, shrinking would only be able to propose numbers greater than 42. ## Transforming generated values Another common need consists of transforming the generated value to a different space, for example the set of all even numbers rather than the (larger) set of integers. Hypothesis allows to do this by passing a lambda to the `map()` method of a strategy: ```python @given(st.integers().map(lambda x: x * 2)) def test_mapping(self, x): self.assertEqual(x % 2, 0) ``` Eris instead provides a `Map` higher-order generator, which applies the lambda during generation: ```php public function testApplyingAFunctionToGeneratedValues() { $this ->forAll( Generator\map( function ($n) { return $n * 2; }, Generator\nat() ) ) ->then(function ($number) { $this->assertTrue( $number % 2 == 0, "$number is not even" ); }); } ``` In both cases, the advantage of using the `map` support from the library (rather than writing our own multiplying code in the tests) is that the resulting object can be further composed to build larger data structures like a list or set of even numbers. ## Generators with random parameters It's possible to build even stricter values, that have internal constraints that must be satisfied but can't easily be generated by applying a pure function to a previously generated value. Hypothesis provides the `flatmap()` method to pass the output of an inner strategy to a lambda that creates an outer strategy to use in the test. Here a list of 4 integers is passed to the lambda, to generate a tuple consisting of the list and a random element chosen from it: ```python @given( st.lists(st.integers(), min_size=4, max_size=4).flatmap( lambda xs: st.tuples(st.just(xs), st.sampled_from(xs)) ) ) def test_list_and_element_from_it(self, pair): (generated_list, element) = pair self.assertIn(element, generated_list) ``` Eris does the same with a slightly different naming, calling this primitive `bind`: ```php public function testCreatingABrandNewGeneratorFromAGeneratedValue() { $this ->forAll( Generator\bind( Generator\vector(4, Generator\nat()), function ($vector) { return Generator\tuple( Generator\elements($vector), Generator\constant($vector) ); } ) ) ->then(function ($tuple) { list($element, $vector) = $tuple; $this->assertContains($element, $vector); }); } ``` ## What the future brings Hypothesis is a much more mature project than Eris, especially when it comes to keeping state between test runs or acting as a generic random data provider rather than as an extension to a testing framework. It will be interesting to continue porting Hypothesis features to the PHP world, given the original features and patterns that Hypothesis shows with respect to the rest of the `*Check` world. ## References The Hypothesis examples shown in this post can be found in [this example repository](https://github.com/giorgiosironi/hypothesis-exploration/blob/master/test_eris.py). The Eris examples can instead be found in the example/ folder of [the](https://github.com/giorgiosironi/eris/blob/master/examples/IntegerTest.php#L9) [Eris](https://github.com/giorgiosironi/eris/blob/master/examples/SequenceTest.php#L31) [repository](https://github.com/giorgiosironi/eris/blob/master/examples/WhenTest.php#L8) [on](https://github.com/giorgiosironi/eris/blob/master/examples/MapTest.php#L8) [GitHub](https://github.com/giorgiosironi/eris/blob/master/examples/BindTest.php#L8). ================================================ FILE: website/content/2016-09-23-hypothesis-3.5.0-release.md ================================================ --- tags: news, python, non-technical date: 2016-09-23 00:00 title: 3.5.0 and 3.5.1 Releases of Hypothesis for Python author: drmaciver --- This is a combined release announcement for two releases. 3.5.0 was released yesterday, and 3.5.1 has been released today after some early bug reports in 3.5.0 ## Changes ### 3.5.0 - 2016-09-22 This is a feature release. * fractions() and decimals() strategies now support min_value and max_value parameters. Thanks go to Anne Mulhern for the development of this feature. * The Hypothesis pytest plugin now supports a --hypothesis-show-statistics parameter that gives detailed statistics about the tests that were run. Huge thanks to [Jean-Louis Fuchs](https://github.com/ganwell) and [Adfinis-SyGroup](https://www.adfinis-sygroup.ch/) for funding the development of this feature. * There is a new event() function that can be used to add custom statistics. Additionally there have been some minor bug fixes: * In some cases Hypothesis should produce fewer duplicate examples (this will mostly only affect cases with a single parameter). * py.test command line parameters are now under an option group for Hypothesis (thanks to David Keijser for fixing this) * Hypothesis would previously error if you used function annotations on your tests under Python 3.4. * The repr of many strategies using lambdas has been improved to include the lambda body (this was previously supported in many but not all cases). ### 3.5.1 - 2016-09-23 This is a bug fix release. * Hypothesis now runs cleanly in -B and -BB modes, avoiding mixing bytes and unicode. * unittest.TestCase tests would not have shown up in the new statistics mode. Now they do. * Similarly, stateful tests would not have shown up in statistics and now they do. * Statistics now print with pytest node IDs (the names you'd get in pytest verbose mode). ## Notes Aside from the above changes, there are a couple big things behind the scenes of this release that make it a big deal. The first is that the flagship chunk of work, statistics, is a long-standing want to have that has never quite been prioritised. By funding it, Jean-Louis and Adfinis-SyGroup successfully bumped it up to the top of the priority list, making it the first funded feature in Hypothesis for Python! Another less significant but still important is that this release marks the first real break with an unofficial Hypothesis for Python policy of not having any dependencies other than the standard library and backports. This release adds a dependency on the uncompyle6 package. This may seem like an odd choice, but it was invaluable for fixing the repr behaviour, which in turn was really needed for providing good statistics for filter and recursive strategies. ================================================ FILE: website/content/2016-10-01-pytest-integration-sponsorship.md ================================================ --- tags: news, python, non-technical date: 2016-10-01 00:00 title: Seeking funding for deeper integration between Hypothesis and pytest author: drmaciver --- Probably the number one complaint I hear from Hypothesis users is that it "doesn't work" with py.test fixtures. [This isn't true](http://hypothesis.works/articles/hypothesis-pytest-fixtures/), but it does have one very specific limitation in how it works that annoys people: It only runs function scoped fixtures once for the entire test, not once per example. Because of the way people use function scoped fixtures for handling stateful things like databases, this often causes people problems. I've been [maintaining for a while](https://github.com/pytest-dev/pytest/issues/916) that this is impossible to fix without some changes on the pytest end. The good news is that this turns out not to be the case. After some conversations with pytest developers, some examining of other pytest plugins, and a bunch of prototyping, I'm pretty sure it's possible. It's just really annoying and a lot of work. So that's the good news. The bad news is that this isn't going to happen without someone funding the work. I've now spent about a week of fairly solid work on this, and what I've got is quite promising: The core objective of running pytest fixtures for every examples works fairly seamlessly. But it's now in the long tail of problems that will need to be squashed before this counts as an actual production ready releasable piece of work. A number of things *don't* work. For example, currently it's running some module scoped fixtures once per example too, which it clearly shouldn't be doing. It also currently has some pretty major performance problems that are bad enough that I would consider them release blocking. As a result I'd estimate there's easily another 2-3 weeks of work needed to get this out the door. Which brings us to the crux of the matter: 2-3 additional weeks of free work on top of the one I've already done is 3-4 weeks more free work than I particularly want to do on this feature, so without sponsorship it's not getting finished. I typically charge £400/day for work on Hypothesis (this is heavily discounted off my normal rates), so 2-3 weeks comes to £4000 to £6000 (roughly $5000 to $8000) that has to come from somewhere. I know there are a number of companies out there using pytest and Hypothesis together. I know from the amount of complaining about this integration that this is a real problem you're experiencing. So, I think this money should come from those companies. Besides helping to support a tool you've already got a lot of value out of, this will expand the scope of what you can easily test with Hypothesis a lot, and will be hugely beneficial to your bug finding efforts. This is a model that has worked well before with the funding of the recent statistics work by [Jean-Louis Fuchs](https://github.com/ganwell) and [Adfinis-SyGroup](https://www.adfinis-sygroup.ch/), and I'm confident it can work well again. If you work at such a company and would like to talk about funding some or part of this development, please email me at [drmaciver@hypothesis.works](mailto:drmaciver@hypothesis.works). ================================================ FILE: website/content/2016-10-17-canonical-serialization.md ================================================ --- tags: python, intro, technical, properties date: 2016-10-17 06:00 title: Another invariant to test for encoders author: drmaciver --- [The encode/decode invariant](../encode-decode-invariant/) is one of the most important properties to know about for testing your code with Hypothesis or other property-based testing systems, because it captures a very common pattern and is very good at finding bugs. But how do you go beyond it? If encoders are that common, surely there must be other things to test with them? The first thing that people tend to try is to simply reverse the order of operations. Continuing the same example from the encode/decode post, this would look something like this: ```python from hypothesis import given from hypothesis.strategies import characters, integers, lists, tuples @given(lists(tuples(characters(), integers(1, 10)))) def test_encode_inverts_decode(s): assert encode(decode(s)) == s ``` But unlike the other way around, this test can fail for reasons that are not obviously errors in the system under test. In particular this will fail with the example [('0', 1), ('0', 1)], because this will be decoded into '00', which will then be encoded into [('0', 2)]. In general, it is quite common to have multiple non-canonical representations of data when encoding: e.g. JSON has non-significant whitespace, an IP address has a wide range of human readable representations, most compressors will evolve to improve their compression over time but will still be able to handle compressed files made with old versions of the code, etc. So this test *shouldn't* be expected to pass. To rescue this, we might imagine we had some sort of function make\_canonical which took an encoded data representation and replaced it with a canonical version of that (e.g. by normalizing whitespace). The test could then look like this: ```python @given(lists(tuples(characters(), integers(1, 10)))) def test_encode_inverts_decode(s): assert make_canonical(encode(decode(s))) == make_canonical(s) ``` But where would we get that make\_canonical function from? It's not something we really want to write ourselves. Fortunately we can put together the pieces we already have to define such a function fairly easily. To see this, lets think about what properties make\_canonical should have. The following seem reasonable: 1. encode should always produce canonical data. i.e. encode(t) == make_canonical(encode(t)) 2. Canonical data should represent the same value. i.e. decode(s) == decode(make_canonical(s)) But we already know that decode(encode(t)) == t from our original property, so we have a natural source of data that is the output of encode: We just decode the data and then encode it again. This gives us the following natural definition of make_canonical: ```python def make_canonical(data): return encode(decode(data)) ``` But this is nearly the same as the thing we were testing, so we can rewrite our test as: ```python @given(lists(tuples(characters(), integers(1, 10)))) def test_encode_inverts_decode(s): assert make_canonical(make_canonical(s)) == make_canonical(s) ``` This property is called being idempotent (annoyingly "idempotent" gets used to mean something subtly different in most API design, but this is the original mathematical meaning). It's less obviously necessary than the original one, and you can certainly write encode/decode pairs that are arguably correct but don't have it (e.g. because they change the order of keys in a dictionary, or include a timestamp or sequence number in the output), but I think it's still worth having and testing. Enforcing consistency like this both helps with debugging when things go wrong and also tends to flush out other bugs along the way. Even if you don't want to enforce this property, it highlights an important issue: You do need *some* sort of testing of the decoder that doesn't just operate on output from the encoder, because the encoder will potentially only output a relatively small subset of the valid range of the format. Often however you'll get the property for free. If the encode and decode functions have the property that whenever x == y then f(x) == f(y), then this property automatically holds, because make_canonical(x) is encode(decode(encode(decode(x)))), and we know from the first property that decode(encode(t)) == t, so with t = decode(x) this expression is encode(decode(x)), which is make_canonical(x) as required. Most encode/decode pairs will have this property, but not all. The easiest ways to fail to have it are to have side-effects (the aforementioned sequence number or randomization), but even without side effects it's possible for it to fail if equality doesn't capture every detail about the type. For example in Python, if 1.0 was serialized as 1, then the two would compare equal and the property would pass, but when re-encoding it might exhibit very different properties (although you'd hope that it wouldn't). Another example is that in Python an OrderedDict and a dict compare equal regardless of iteration order, which means that two apparently equal types might encode to different things if they have different iteration orders defined. Ultimately these issues are probably quite niche. It's likely still worth testing for this property, both because of these problems and also because often [mathematically equivalent properties can still catch different issues](../tests-as-complete-specifications/), but it's significantly less important than the more general property we started with. -------------------------------------- Thanks to [Georges Dubus](https://twitter.com/georgesdubus) who pointed out the key insight behind the last section on this property following from the original one. ================================================ FILE: website/content/2016-10-31-hypothesis-3.6.0-release.md ================================================ --- tags: news, python, non-technical date: 2016-10-31 00:00 title: 3.6.0 Release of Hypothesis for Python author: drmaciver --- This is a release announcement for the 3.6.0 release of Hypothesis for Python. It's a bit of an emergency release. Hypothesis 3.5.0 inadvertently added a dependency on GPLed code (see below for how this happened) which this release removes. This means that if you are running Hypothesis 3.5.x then there is a good chance you are in violation of the GPL and you should update immediately. Apologies for any inconvenience this may have caused. ### From the Changelog This release reverts Hypothesis to its old pretty printing of lambda functions based on attempting to extract the source code rather than decompile the bytecode. This is unfortunately slightly inferior in some cases and may result in you occasionally seeing things like `lambda x: ` in statistics reports and strategy reprs. This removes the dependencies on uncompyle6, xdis and spark-parser. The reason for this is that the new functionality was based on uncompyle6, which turns out to introduce a hidden GPLed dependency - it in turn depended on xdis, and although the library was licensed under the MIT license, it contained some GPL licensed source code and thus should have been released under the GPL. My interpretation is that Hypothesis itself was never in violation of the GPL (because the license it is under, the Mozilla Public License v2, is fully compatible with being included in a GPL licensed work), but I have not consulted a lawyer on the subject. Regardless of the answer to this question, adding a GPLed dependency will likely cause a lot of users of Hypothesis to inadvertently be in violation of the GPL. As a result, if you are running Hypothesis 3.5.x you really should upgrade to this release immediately. ### Notes This Halloween release brought to you by the specter of inadvertent GPL violations (but sadly this is entirely real and neither trick nor treat). This dependency also caused a number of other problems, so in many ways its not entirely a bad thing that it's gone, but it's still sad to remove functionality. At some point in the future I will try to restore the lost functionality, but doing it without access to uncompyle6 will be a moderate amount of work, so it's not going to be high priority in the near future. ================================================ FILE: website/content/2016-12-05-integrated-shrinking.md ================================================ --- tags: technical, details, python, alternatives date: 2016-12-05 10:00 title: Integrated vs type based shrinking author: drmaciver --- One of the big differences between Hypothesis and Haskell QuickCheck is how shrinking is handled. Specifically, the way shrinking is handled in Haskell QuickCheck is bad and the way it works in Hypothesis (and also in test.check and EQC) is good. If you're implementing a property based testing system, you should use the good way. If you're using a property based testing system and it doesn't use the good way, you need to know about this failure mode. Unfortunately many (and possibly most) implementations of property based testing are based on Haskell's QuickCheck and so make the same mistake. The big difference is whether shrinking is integrated into generation. In Haskell's QuickCheck, shrinking is defined based on *types*: Any value of a given type shrinks the same way, regardless of how it is generated. In Hypothesis, test.check, etc. instead shrinking is part of the generation, and the generator controls how the values it produces shrinks (this works differently in Hypothesis and test.check, and probably differently again in EQC, but the user visible result is largely the same) This is not a trivial distinction. Integrating shrinking into generation has two large benefits: * Shrinking composes nicely, and you can shrink anything you can generate regardless of whether there is a defined shrinker for the type produced. * You can guarantee that shrinking satisfies the same invariants as generation. The first is mostly important from a convenience point of view: Although there are some things it let you do that you can't do in the type based approach, they're mostly of secondary importance. It largely just saves you from the effort of having to write your own shrinkers. But the second is *really* important, because the lack of it makes your test failures potentially extremely confusing. To see this, lets consider the following example in Hypothesis: ```python from hypothesis import given from hypothesis.strategies import integers even_numbers = integers().map(lambda x: x * 2) @given(even_numbers) def test_even_numbers_are_even(n): assert n % 2 == 0 ``` This test always passes: We generate an even number by multiplying an integer we've generated by two. No problem. Now suppose we made the test fail: ```python from hypothesis import given from hypothesis.strategies import integers even_numbers = integers().map(lambda x: x * 2) @given(even_numbers) def test_even_numbers_are_even(n): assert n % 2 == 0 assert n <= 4 ``` This test will of course fail: Any value of n which is at least 5 will fail the test. But which assertion will fail, and what value will cause it to fail? In Hypothesis it's the second one, and it will fail with n=6: The numbers passed in will still always be even integers, and the smallest even integer which fails the test is 6. But suppose Hypothesis implemented type based shrinking (very early versions of it *did* implement type based shrinking, but this stopped being the case somewhere well before 1.0 when the API looked very different). In that case Hypothesis would just know that these things that were failing the tests were integers, so it would say "How about 1? 1 is a perfectly valid integer. Lets try 1". It would pass in n=1, the first assertion would trigger, and the test would promptly fail. A successful shrink! But it's completely not what we wanted. We were just trying to test on even integers and the shrinking messed this up. This is in some sense the classic [shrinkers are fuzzers](http://blog.regehr.org/archives/1284) problem where an error is reduced to a different error, but it's a particularly annoying version of that because an error we care about is being reduced to an error we don't care about. So we have to duplicate the constraint logic in our test to make this work: ```python from hypothesis import assume, given from hypothesis.strategies import integers even_numbers = integers().map(lambda x: x * 2) @given(even_numbers) def test_even_numbers_are_even(n): assume(n % 2 == 0) assert n % 2 == 0 assert n <= 4 ``` (Having both the assume and the first assert there is of course redundant) In this example the problem was relatively obvious and so easy to work around, but as your invariants get more implicit and subtle it becomes really problematic: In Hypothesis it's easy and convenient to [generate quite complex data](../generating-the-right-data/), and trying to recreate the invariants that are automatically satisfied with that in your tests and/or your custom shrinkers would quickly become a nightmare. I don't think it's an accident that the main systems to get this right are in dynamic languages. It's certainly not *essential* - [the original proposal that led to the implementation for test.check was for Haskell](https://mail.haskell.org/pipermail/libraries/2013-November/021674.html), and [Jack](https://github.com/ambiata/disorder.hs/tree/master/disorder-jack) is an alternative property based system for Haskell that does this - but you feel the pain much more quickly in dynamic languages because the typical workaround for this problem in Haskell is to define a newtype, which lets you turn off the default shrinking for your types and possibly define your own. But that's a workaround for a problem that shouldn't be there in the first place, and using it will still result in your having to encode the invariants into your your shrinkers, which is more work and more brittle than just having it work automatically. So although (as far as I know) none of the currently popular property based testing systems for statically typed languages implement this behaviour correctly, they absolutely can and they absolutely should. It will improve users' lives significantly. This of course goes doubly for dynamic languages, where even working around this problem is hard. So, in conclusion, just say no to type based shrinking. It's a bad idea, and failures your property based tests will become significantly harder to understand in its presence. ================================================ FILE: website/content/2016-12-08-compositional-shrinking.md ================================================ --- tags: technical, details, python, alternatives date: 2016-12-08 9:00 title: Compositional shrinking author: drmaciver --- In [my last article about shrinking](../integrated-shrinking/), I discussed the problems with basing shrinking on the type of the values to be shrunk. In writing it though I forgot that there was a halfway house which is also somewhat bad (but significantly less so) that you see in a couple of implementations. This is when the shrinking is not type based, but still follows the classic shrinking API that takes a value and returns a lazy list of shrinks of that value. Examples of libraries that do this are [theft](https://github.com/silentbicycle/theft) and [QuickTheories](https://github.com/NCR-CoDE/QuickTheories). This works reasonably well and solves the major problems with type directed shrinking, but it's still somewhat fragile and importantly does not compose nearly as well as the approaches that Hypothesis or test.check take. Ideally, as well as not being based on the types of the values being generated, shrinking should not be based on the actual values generated at all. This may seem counter-intuitive, but it actually works pretty well. Rather than going into implementation details just yet, lets start with why this is important. Consider the example from the last post: ```python from hypothesis import given from hypothesis.strategies import integers even_numbers = integers().map(lambda x: x * 2) @given(even_numbers) def test_even_numbers_are_even(n): assert n % 2 == 0 ``` We took a strategy and composed it with a function mapping over the values that that strategy produced to get a new strategy. Suppose the Hypothesis strategy implementation looked something like the following: ```python class SearchStrategy: def generate(self, random): raise NotImplementedErro() def shrink(self, value): return () ``` i.e. we can generate a value and we can shrink a value that we've previously generated. By default we don't know how to generate values (subclasses have to implement that) and we can't shrink anything, which subclasses are able to fix if they want or leave as is if they're fine with that. (This is in fact how a very early implementation of it looked) This is essentially the approach taken by theft or QuickTheories, and the problem with it is that under this implementation the 'map' function we used above is impossible to define in a way that preserves shrinking: In order to shrink a generated value, you need some way to invert the function you're composing with (which is in general impossible even if your language somehow exposed the facilities to do it, which it almost certainly doesn't) so you could take the generated value, map it back to the value that produced it, shrink that and then compose with the mapping function. Hypothesis and test.check both support even more complicated composition of strategies (Hypothesis somewhat better than test.check - both support the same operations, but Hypothesis's underlying implementation works somewhat better for more complicated compositions), but even the simplest of compositions fails if you need to be able to shrink arbitrary values. The key idea for fixing this is as follows: In order to shrink *outputs* it almost always suffices to shrink *inputs*. Although in theory you can get functions where simpler input leads to more complicated output, in practice this seems to be rare enough that it's OK to just shrug and accept more complicated test output in those cases. Given that, the way to shrink the output of a mapped strategy is to just shrink the value generated from the first strategy and feed it to the mapping function. Which means that you need an API that can support that sort of shrinking. The way this works in test.check is that instead of generating a single value it generates an entire (lazy) tree of values with shrinks for them. See [Reid Draper's article on the subject](http://reiddraper.com/writing-simple-check/) for slightly more detail. This supports mapping fairly easily: We just apply the mapping function to the rose tree - both the initial generated value, and all the shrunk child values. Hypothesis's implementation is more complicated so will have to wait for another article, but the key idea behind it is that Hypothesis takes the "Shrinking outputs can be done by shrinking inputs" idea to its logical conclusion and has a single unified intermediate representation that *all* generation is based off. Strategies can provide hints about possibly useful shrinks to perform on that representation, but otherwise have very little control over the shrinking process at all. This supports mapping even more easily, because a strategy is just a function which takes an IR object and returns a value, so the mapped strategy just does the same thing and applies the mapping function. Obviously I think Hypothesis's implementation is better, but test.check's implementation is entirely respectable too and is probably easier to copy right now if you're implementing a property based testing system from scratch. But I do think that whichever one you start from it's important to take away the key idea: You can shrink outputs by shrinking inputs, and strategies should compose in a way that preserves shrinking. The result is significantly more convenient to use because it means that users will rarely or never have to write their own shrinking functions, and there are fewer possible places for shrinking and generation to get out of sync. ================================================ FILE: website/content/2016-12-10-how-hypothesis-works.md ================================================ --- tags: python, details, technical date: 2016-12-10 11:00 title: How Hypothesis Works author: drmaciver --- Hypothesis has a very different underlying implementation to any other property-based testing system. As far as I know, it's an entirely novel design that I invented. Central to this design is the following feature set which *every* Hypothesis strategy supports automatically (the only way to break this is by having the data generated depend somehow on external global state): 1. All generated examples can be safely mutated 2. All generated examples can be saved to disk (this is important because Hypothesis remembers and replays previous failures). 3. All generated examples can be shrunk 4. All invariants that hold in generation must hold during shrinking ( though the probability distribution can of course change, so things which are only supported with high probability may not be). (Essentially no other property based systems manage one of these claims, let alone all) The initial mechanisms for supporting this were fairly complicated, but after passing through a number of iterations I hit on a very powerful underlying design that unifies all of these features. It's still fairly complicated in implementation, but most of that is optimisations and things needed to make the core idea work. More importantly, the complexity is quite contained: A fairly small kernel handles all of the complexity, and there is little to no additional complexity (at least, compared to how it normally looks) in defining new strategies, etc. This article will give a high level overview of that model and how it works. Hypothesis consists of essentially three parts, each built on top of the previous: 1. A low level interactive byte stream fuzzer called *Conjecture* 2. A strategy library for turning Conjecture's byte streams into high level structured data. 3. A testing interface for driving test with data from Hypothesis's strategy library. I'll focus purely on the first two here, as the latter is complex but mostly a matter of plumbing. The basic object exposed by Conjecture is a class called TestData, which essentially looks like an open file handle you can read bytes from: ```python class TestData: def draw_bytes(self, n): ... ``` (note: The Python code in this article isn't an exact copy of what's found in Hypothesis, but has been simplified for pedagogical reasons). A strategy is then just an object which implements a single abstract method from the strategy class: ```python class SearchStrategy: def do_draw(self, data): raise NotImplementedError() ``` The testing interface then turns test functions plus the strategies they need into something that takes a TestData object and returns True if the test fails and False if it passes. For a simple example, we can implement a strategy for unsigned 64-bit integers as follows: ```python class Int64Strategy: def do_draw(self, data): return int.from_bytes(data.draw_bytes(8), byteorder="big", signed=False) ``` As well as returning bytes, draw_bytes can raise an exception that stops the test. This is useful as a way to stop examples from getting too big (and will also be necessary for shrinking, as we'll see in a moment). From this it should be fairly clear how we support saving and mutation: Saving every example is possible because we can just write the bytes that produced it to disk, and mutation is possible because strategies are just returning values that we don't in any way hang on to. But how does shrinking work? Well the key idea is the one I mentioned in [my last article about shrinking ](../compositional-shrinking/) - shrinking inputs suffices to shrink outputs. In this case the input is the byte stream. Once Hypothesis has found a failure it begins shrinking the byte stream using a TestData object that looks like the following: ```python class ShrinkingTestData: def __init__(self, data): self.data = data self.index = 0 def draw_bytes(self, n): if self.index + n > len(self.data): raise StopTest() result = self.data[self.index : self.index + n] self.index += n return result ``` Shrinking now reduces to shrinking the byte array that gets passed in as data, subject to the condition that our transformed test function still returns True. Shrinking of the byte array is designed to try to minimize it according to the following rules: 1. Shorter is always simpler. 2. Given two byte arrays of the same length, the one which is lexicographically earlier (considering bytes as unsigned 8 bit integers) is simpler. You can imagine that some variant of [Delta Debugging](https://en.wikipedia.org/wiki/Delta_Debugging) is used for the purpose of shrinking the byte array, repeatedly deleting data and lowering bytes until no byte may be deleted or lowered. It's a lot more complicated than that, but I'm mostly going to gloss over that part for now. As long as the strategy is well written (and to some extent even when it's not - it requires a certain amount of active sabotage to create strategies that produce more complex data given fewer bytes) this results in shrinks to the byte array giving good shrinks to the generated data. e.g. our 64-bit unsigned integers are chosen to be big endian so that shrinking the byte data lexicographically shrinks the integer towards zero. In order to get really good deleting behaviour in our strategies we need to be a little careful about how we arrange things, so that deleting in the underlying bytestream corresponds to deleting in generated data. For example, suppose we tried to implement lists as follows: ```python class ListStrategy(SearchStrategy): def __init__(self, elements): super().__init__() self.elements = elements def do_draw(self, data): n_elements = integers(0, 10).do_draw(self.elements) return [self.elements.do_draw(data) for _ in range(n_elements)] ``` The problem with this is that deleting data doesn't actually result in deleting elements - all that will happen is that drawing will run off the end of the buffer. You can potentially shrink n_elmements, but that only lets you delete things from the end of the list and will leave a bunch of left over data at the end if you do - if this is the last data drawn that's not a problem, and it might be OK anyway if the data usefully runs into the next strategy, but it works fairly unreliably. I am in fact working on an improvement to how shrinking works for strategies that are defined like this - they're quite common in user code, so they're worth supporting - but it's better to just have deletion of elements correspond to deletion of data in the underlying bytestream. We can do this as follows: ```python class ListStrategy(SearchStrategy): def __init__(self, elements): super().__init__() self.elements = elements def do_draw(self, data): result = [] while booleans().do_draw(data): result.append(self.elements.do_draw(data)) return result ``` We now draw lists as a series True, element, True, element, ..., False, etc. So if you delete the interval in the byte stream that starts with a True and finishes at the end of an element, that just deletes that element from the list and shifts everything afterwards left one space. Given some careful strategy design this ends up working pretty well. It does however run into problems in two minor cases: 1. It doesn't generate very good data 2. It doesn't shrink very well Fortunately both of these are fixable. The reason for the lack of good data is that Conjecture doesn't know enough to produce a good distribution of bytes for the specific special values for your strategy. e.g. in our unsigned 64 bit integer examples above it can probably guess that 0 is a special value, but it's not necessarily obvious that e.g. focusing on small values is quite useful. This gets worse as you move further away from things that look like unsigned integers. e.g. if you're turning bytes into floats, how is Conjecture supposed to know that Infinity is an interesting value? The simple solution is to allow the user to provide a distribution hint: ```python class TestData: def draw_bytes(self, n, distribution=None): ... ``` Where a distribution function takes a Random object and a number of bytes. This lets users specify the distribution of bytes. It won't necessarily be respected - e.g. it certainly isn't in shrinking, but the fuzzer can and does mutate the values during generation too - but it provides a good starting point which allows you to highlight special values, etc. So for example we could redefine our integer strategy as: ```python class Int64Strategy: def do_draw(self, data): def biased_distribution(random, n): if random.randint(0, 1): return random.randint(0, 100).to_bytes(n, byteorder="big", signed=False) else: return uniform(random, n) return int.from_bytes( data.draw_bytes(8, biased_distribution), byteorder="big", signed=False ) ``` Now we have a biased integer distribution which will produce integers between 0 and 100 half the time. We then use the strategies to generate our initial buffers. For example we could pass in a TestData implementation that looked like this: ```python class GeneratingTestData(TestData): def __init__(self, random, max_bytes): self.max_bytes = max_bytes self.random = random self.record = bytearray() def draw_bytes(self, n, distribution): if n + len(self.record) > self.max_bytes: raise StopTest() result = distribution(self.random, n) self.record.extend(result) return result ``` This draws data from the provided distribution and records it, so at the end we have a record of all the bytes we've drawn so that we can replay the test afterwards. This turns out to be mostly enough. I've got some pending research to replace this API with something a bit more structured (the ideal would be that instead of opaque distribution objects you draw from an explicit mixture of grammars), but for the moment research on big changes like that is side lined because nobody is funding Hypothesis development, so I've not got very far with it. Initial designs tried to avoid this approach by using data from the byte stream to define the distribution, but this ended up producing quite opaque structures in the byte stream that didn't shrink very well, and this turned out to be simpler. The second problem of it not shrinking well is also fairly easily resolved: The problem is not that we *can't* shrink it well, but that shrinking ends up being slow because we can't tell what we need to do: In our lists example above, the only way we currently have to delete elements is to delete the corresponding intervals, and the only way we have to find the right intervals is to try *all* of them. This potentially requires O(n^2) deletions to get the right one. The solution is just to do a bit more book keeping as we generate data to mark useful intervals. TestData now looks like this: ```python class TestData: def start_interval(self): ... def stop_interval(self): ... def draw_bytes(self, n): ... def draw(self, strategy): self.start_interval() result = strategy.do_draw(self) self.stop_interval() return result ``` We then pass everything through data.draw instead of strategy.do_draw to maintain this bookkeeping. These mark useful boundaries in the bytestram that we can try deleting: Intervals which don't cross a value boundary are much more likely to be useful to delete. There are a large number of other details that are required to make Hypothesis work: The shrinker and the strategy library are both carefully developed to work together, and this requires a fairly large number of heuristics and special cases to make things work, as well as a bunch of book keeping beyond the intervals that I've glossed over. It's not a perfect system, but it works and works well: This has been the underlying implementation of Hypothesis since the 3.0 release in early 2016, and the switch over was nearly transparent to end users: the previous implementation was much closer to a classic QuickCheck model (with a great deal of extra complexity to support the full Hypothesis feature set). In a lot of cases it even works better than heavily customized solutions: For example, a benefit of the byte based approach is that all parts of the data are fully comprehensible to it. Often more structured shrinkers get stuck in local minima because shrinking one part of the data requires simultaneously shrinking another part of the data, whileas Hypothesis can just spot patterns in the data and speculatively shrink them together to see if it works. The support for chaining data generation together is another thing that benefits here. In Hypothesis you can chain strategies together like this: ```python class SearchStrategy: def do_draw(self, data): raise NotImplementedError() def flatmap(self, bind): return FlatmappedStrategy(self, bind) class FlatmappedStrategy(SearchStrategy): def __init__(self, base, bind): super().__init__() self.base = base self.bind = bind def do_draw(self, data): value = data.draw(self.base) return data.draw(self.bind(value)) ``` The idea is that flatmap lets you chain strategy definitions together by drawing data that is dependent on a value from other strategies. This works fairly well in modern Hypothesis, but has historically (e.g. in test.check or pre 3.0 Hypothesis) been a problem for integrated testing and generation. The reason this is normally a problem is that if you shrink the first value you've drawn then you essentially *have* to invalidate the value drawn from bind(value): There's no real way to retain it because it came from a completely different generator. This potentially results in throwing away a lot of previous work if a shrink elsewhere suddenly makes it to shrink the initial value. With the Hypothesis byte stream approach this is mostly a non-issue: As long as the new strategy has roughly the same shape as the old strategy it will just pick up where the old shrinks left off because they operate on the same underlying byte stream. This sort of structure *does* cause problems for Hypothesis if shrinking the first value would change the structure of the bound strategy too much, but in practice it usually seems to work out pretty well because there's enough flexibility in how the shrinks happen that the shrinker can usually work past it. This model has proven pretty powerful even in its current form, but there's also a lot of scope to expand it. But hopefully not by too much. One of the advantages of the model in its current form though is its simplicity. The [Hypothesis for Java prototype](https://github.com/HypothesisWorks/hypothesis-java) was written in an afternoon and is pretty powerful. The whole of the Conjecture implementation in Python is a bit under a thousand significant lines of fairly portable code. Although the strategy library and testing interface are still a fair bit of work, I'm still hopeful that the Hypothesis/Conjecture approach is the tool needed to bring an end to the dark era of property based testing libraries that don't implement shrinking at all. ================================================ FILE: website/content/2017-03-09-hypothesis-for-researchers.md ================================================ --- tags: python, details, technical date: 2017-03-09 11:00 title: Hypothesis for Computer Science Researchers author: drmaciver --- I'm in the process of [trying to turn my work on Hypothesis into a PhD](http://www.drmaciver.com/2017/03/looking-into-starting-a-phd/) and I realised that I don't have a good self-contained summary as to why researchers should care about it. So this is that piece. I'll try to give a from scratch introduction to the why and what of Hypothesis. It's primarily intended for potential PhD supervisors, but should be of general interest as well (especially if you work in this field). ### Why should I care about Hypothesis from a research point of view? The short version: Hypothesis takes an existing effective style of testing (property-based testing) which has proven highly effective in practice and makes it accessible to a much larger audience. It does so by taking several previously unconnected ideas from the existing research literature on testing and verification, and combining them to produce a novel implementation that has proven very effective in practice. The long version is the rest of this article. The remainder is divided into several sections: * [What is Hypothesis?](#what-is-hypothesis) is a from-scratch introduction to Hypothesis. If you are already familiar with property-based testing (e.g. from QuickCheck) you can probably skip this. * [How is Hypothesis innovative?](#how-is-hypothesis-innovative) is about the current state of the art of Hypothesis and why it's interesting. If you've already read [How Hypothesis Works](http://hypothesis.works/articles/how-hypothesis-works/) this is unlikely to teach you anything new and you can skip it. * [What prior art is it based on?](#what-prior-art-is-it-based-on) is a short set of references for some of the inspirations for Hypothesis. You probably shouldn't skip this, because it's short and the linked material is all interesting. * [What are some interesting research directions?](#what-are-some-interesting-research-directions) explores possible directions I'm looking into for the future of Hypothesis, some of which I would hope to include in any PhD related to it that I worked on. You probably shouldn't skip this if you care about this document at all. * [What should you do with this information?](#what-should-you-do-with-this-information) simply closes off the article and winds things down. So, without further ado, the actual content. ### What is Hypothesis? Hypothesis is an implementation of *property-based testing*, an idea that originated with a Haskell library called QuickCheck. Property-based testing is a way to augment your unit tests with a source of structured random data that allows a tool to explore the edge cases of your tests and attempt to find errors automatically. I've made a [longer and more formal discussion](http://hypothesis.works/articles/what-is-property-based-testing/) of this definition in the past. An example of a property-based test using Hypothesis: ```python from hypothesis import given, strategies as st @given(st.lists(st.integers())) def test_sort_is_idempotent(ls): sort1 = sorted(ls) assert sorted(sort1) == sort1 ``` This exposes a normal function which can be picked up by a standard runner such as py.test. You can also just call it directly: ```python if __name__ == "__main__": test_sort_is_idempotent() ``` When the test is run, Hypothesis will generate random lists of integers and pass them to the test. The test sorts the integers, then sorts them again, and asserts that the two results are the same. As long as the test passes for every input Hypothesis feeds it this will appear to be a normal test. If it fails however, Hypothesis will then repeatedly rerun it with progressively simpler examples to try and find a minimal input that causes the failure. To see this, suppose we implemented the following rather broken implementation of sorted: ```python def sorted(ls): return list(reversed(ls)) ``` Then on running we would see the following output: ``` @given(st.lists(st.integers())) def test_sort_is_idempotent(ls): sort1 = sorted(ls) > assert sorted(sort1) == sort1 E assert [0, 1] == [1, 0] E At index 0 diff: 0 != 1 E Use -v to get the full diff sorting.py:12: AssertionError ---- Hypothesis ---- Falsifying example: test_sort_is_idempotent(ls=[0, 1]) ``` Hypothesis probably started with a much more complicated example (the test fails for essentially any list with more than one element) and then successfully reduced it to the simplest possible example: A list with two distinct elements. Importantly, when the test is rerun, Hypothesis will start from the falsifying example it found last time rather than trying to generate and shrink a new one from scratch. In this particular case that doesn't matter very much - the example is found very quickly and it always finds the same one - but for more complex and slower tests this is an vital part of the development workflow: It means that tests run much faster and don't stop failing until the bug is actually fixed. Tests can also draw more data as they execute: ```python @given(st.lists(st.integers(), min_size=1), st.data()) def test_sort_is_idempotent(ls, data): ls.sort() i = data.draw(st.integers(0, len(ls) - 1)) assert ls[i - 1] <= ls[i] ``` This fails because we've forgotten than `i` may be zero, and also about Python's negative indexing of lists: ``` @given(st.lists(st.integers(), min_size=1), st.data()) def test_sort_is_idempotent(ls, data): ls.sort() i = data.draw(st.integers(0, len(ls) - 1)) > assert ls[i - 1] <= ls[i] E assert 1 <= 0 sorting.py:15: AssertionError ---- Hypothesis ---- Falsifying example: test_sort_is_idempotent(ls=[0, 1], data=data(...)) Draw 1: 0 ``` Simplification and example saving work as normal for data drawn in this way. Hypothesis also has a form of [model based testing](http://hypothesis.works/articles/rule-based-stateful-testing/), in which you specify a set of valid operations on your API and it attempts to generate whole programs using those operations and find a simple one that breaks. ### How is Hypothesis innovative? From an end user point of view, Hypothesis adds several important things: * It exists at all and people use it. Historically this sort of testing has been found mostly within the functional programming community, and attempts to make it work in other languages have not seen much success or widespread adoption. Some of this is due to novel implementation details in Hypothesis, and some is due to design decisions making it "feel" like normal testing instead of formal methods. * Specifying data generators is much easier than in traditional QuickCheck methods, and you get a great deal more functionality "for free" when you do. This is similar to [test.check](https://github.com/clojure/test.check) for Clojure, or indeed to the [Erlang version of QuickCheck](http://www.quviq.com/products/erlang-quickcheck/), but some of the design decisions of Hypothesis make it significantly more flexible here. * The fact that arbitrary examples can be saved and replayed significantly improves the development work-flow. Other implementations of property-based testing either don't do this at all, only save the seed, or rely on being able to serialize the generated objects (which can break invariants when reading them back in). * The fact that you can generate additional data within the test is often extremely useful, and seems to be unique to Hypothesis in this category of testing tool. These have worked together well to fairly effectively bring property based testing "to the masses", and Hypothesis has started to see increasingly widespread use within the Python community, and is being actively used in the development of tools and libraries, as well as in the development of both CPython and pypy, the two major implementations of Python. Much of this was made possible by Hypothesis's novel implementation. From an implementation point of view, the novel feature of Hypothesis is this: Unlike other implementations of property-based testing, it does not need to understand the structure of the data it is generating at all (it sometimes has to make guesses about it, but its correctness is not dependent on the accuracy of those guesses). Hypothesis is divided into three logically distinct parts: 1. A core engine called *Conjecture*, which can be thought of as an interactive fuzzer for lightly structured byte streams. 2. A strategy library, which is designed to take Conjecture's output and turn it into arbitrary values representable in the programming language. 3. An interface to external test runners that takes tests built on top of the strategy library and runs them using Conjecture (in Python this mostly just consists of exposing a function that the test runners can pick up, but in the [Java Prototype](http://github.com/HypothesisWorks/hypothesis-java) this is more involved and ends up having to interact with some interesting JUnit specific features. Conjecture is essentially the interesting part of Hypothesis's implementation and is what supports most of its functionality: Generation, shrinking, and serialization are all built into the core engine, so implementations of strategies do not require any awareness of these features to be correct. They simply repeatedly ask the Conjecture engine for blocks of bytes, which it duly provides, and they return the desired result. If you want to know more about this, I have previously written [How Hypothesis Works](http://hypothesis.works/articles/how-hypothesis-works/), which provides a bit more detail about Conjecture and how Hypothesis is built on top of it. ### What prior art is it based on? I've done a fair bit of general reading of the literature in the course of working on Hypothesis. The two main papers on which Hypothesis is based are: * [QuickCheck: a lightweight tool for random testing of Haskell programs](https://dl.acm.org/citation.cfm?id=351266) essentially started the entire field of property-based testing. Hypothesis began life as a QuickCheck implementation, and its user facing API continues to be heavily based on QuickCheck, even though the implementation has diverged very heavily from it. * [EXPLODE: a lightweight, general system for finding serious storage system errors](https://dl.acm.org/citation.cfm?id=1298469) provided the key idea on which the Conjecture engine is based - instead of doing static data generation separate from the tests, provide tests with an interactive primitive from which they can draw data. Additionally, the following are major design inspirations in the Conjecture engine, although their designs are not currently used directly: * [American Fuzzy Lop](http://lcamtuf.coredump.cx/afl/) is an excellent security-oriented fuzzer, although one without much academic connections. I've learned a fair bit about the design of fuzzers from it. For a variety of pragmatic reasons I don't currently use its most important innovation (branch coverage metrics as a tool for corpus discovery), but I've successfully prototyped implementations of that on top of Hypothesis which work pretty well. * [Swarm Testing](https://dl.acm.org/citation.cfm?id=2336763) drove a lot of the early designs of Hypothesis's data generation. It is currently not explicitly present in the Conjecture implementation, but some of what Conjecture does to induce deliberate correlations in data is inspired by it. ### What are some interesting research directions? I have a large number of possible directions that my work on Hypothesis could be taken. None of these are *necessarily* a thing that would be the focus of a PhD - in doing a PhD I would almost certainly focus on a more specific research question that might include some or all of them. These are just areas that I am interested in exploring which I think might form an interesting starting point, and whatever focus I actually end up with will likely be more carefully tailored in discussion with my potential supervisors. One thing that's also worth considering: Most of these research directions are ones that would result in improvements to Hypothesis without changing its public interface. This results in a great practical advantage to performing the research because of the relatively large (and ever-growing) corpus of open source projects which are already using Hypothesis - many of these changes could at least partly be validated by just running peoples' existing tests and seeing if any new and interesting bugs are found! Without further ado, here are some of what I think are the most interesting directions to go next. #### More structured byte streams My current immediate research focus on Hypothesis is to replace the core Conjecture primitive with a more structured one that bears a stronger resemblance to its origins in EXPLODE. This is designed to address a number of practical problems that Hypothesis users currently experience (mostly performance related), but it also opens up a number of other novel abstractions that can be built on top of the core engine. The idea is to pare down the interface so that when calling in to Conjecture you simply draw a single byte, specifying a range of possible valid bytes. This gives Conjecture much more fine-grained information to work with, which opens up a number of additional features and abstractions that can be built on top of it. From this primitive you can then rebuild arbitrary weighted samplers that shrink correctly (using a variation of the Alias Method), and arbitrary grammars (probably using [Boltzmann Samplers](https://dl.acm.org/citation.cfm?id=1024669) or similar). This will provide a much more thorough basis for high quality data generation than the current rather ad hoc method of specifying byte streams. This is perhaps more engineering than research, but I think it would at the bare minimum make any paper I wrote about the core approach of Hypothesis significantly more compelling, and it contains a number of interesting applications of the theory. #### Glass box testing Currently Conjecture treats the tests it calls as a black box and does not get much information about what the tests it executes are actually doing. One obvious thing to do which brings in some more ideas from e.g. American Fuzzy Lop is to use more coverage information, but so far I haven't had much success with making my prototypes of this idea suitable for real world use. The primary reason for this so far has been that all of the techniques I've found have worked well when tests are allowed to run for minutes or hours, but the current design focus of Hypothesis assumes tests have seconds to run at most, which limits the utility of these methods and means they haven't been a priority so far. But in principle this should be an extremely profitable line of attack, even with that limitation, and I would like to explore it further. The main idea would be to add a notion of "tags" to the core Conjecture engine which could be used to guide the search. Coverage would be one source of tags, but others are possible. For example, my previous work on [Schroedinteger](https://github.com/DRMacIver/schroedinteger) implements what is essentially a form of lightweight [Concolic testing](https://en.wikipedia.org/wiki/Concolic_testing) that would be another possibly interesting source of information to use. Exactly how much of this is original research and how much is just applications of existing research is yet to be determined, but I think it very likely that at the very least figuring out how to make use of this sort of information in sharply bounded time is likely to bear interesting fruit. The opportunity to see how Concolic testing behaves in the wild is also likely to result in a number of additional questions. #### Making the Conjecture engine smarter A thing I've looked into in the past is the possible use of grammar inference to improve shrinking and data generation. At the time the obstacle I ran into was that the algorithm I was using - [an optimized variation of L\* search](https://dl.acm.org/citation.cfm?id=73047) - did not get good performance in practice on the problems I tried it on. [Synthesizing Program Input Grammars](https://arxiv.org/abs/1608.01723) promises to lift this restriction by providing much better grammar inference in practical scenarios that are quite closely related to this problem domain, so I would like to revisit this and see if it can prove useful. There are likely a number of other ways that the Conjecture engine can probe the state of the system under test to determine interesting potential behaviours, especially in combination with glass box testing features. I think there are a lot of potentially interesting research directions in here - especially if this is combined with the glass box testing. Given that I haven't even been able to make this perform acceptably in the past, the first one would be to see if I can! This will also require a fair bit of practical experimentation to see what works well at actually finding bugs and what doesn't. This is one area in particular where a corpus of open source projects tested with Hypothesis will be extremely helpful. #### Other testing abstractions Despite Hypothesis primarily being a library for property based testing, the core Conjecture engine actually has very little to do with property-based testing and is a more powerful low-level testing abstraction. It would be interesting to see how far that could be taken - the existing stateful/model-based testing is one partial step in that direction, but it could also potentially be used more directly for other things. e.g. in tandem with some of the above features it could be used for low-level fuzzing of binaries, or using it to drive thread scheduling. The nice thing about the Conjecture separation is that because it is so self-contained, it can be used as the core building block on which other tools can be rebuilt and gain a lot of its major features for free. I don't currently have any concrete plans in this direction, but it seems likely there are some interesting possibilities here that will emerge after more review of the testing literature. This is probably just engineering unless some particularly interesting application emerges, but I think the basic potential of the technology would probably give pretty good odds of such an application. ### What should you do with this information? It depends who you are. * If I'm already talking to you because you're a potential PhD supervisor, tell me what about this interests you and ask me lots of questions. * If you're a potential PhD supervisor who I'm *not* already talking to but you'd like me to, please let me know! * If you're somebody else, it's rather up to you. Feel free to send me papers, questions, etc. Whoever you are, if you found this document interesting I'd love to hear from you. Drop me an email at [david@drmaciver.com](mailto:david@drmaciver.com). ================================================ FILE: website/content/2017-04-05-how-not-to-die-hard-with-hypothesis.md ================================================ --- tags: python, technical, intro date: 2017-04-05 10:00 title: Solving the Water Jug Problem from Die Hard 3 with TLA+ and Hypothesis author: nchammas --- _This post was originally published on [the author's personal site](http://nchammas.com/writing/how-not-to-die-hard-with-hypothesis). It is reproduced here with his permission._ In the movie [Die Hard with a Vengeance](https://en.wikipedia.org/wiki/Die_Hard_with_a_Vengeance) (aka Die Hard 3), there is [this famous scene](https://www.youtube.com/watch?v=6cAbgAaEOVE) where John McClane (Bruce Willis) and Zeus Carver (Samuel L. Jackson) are forced to solve a problem or be blown up: Given a 3 gallon jug and 5 gallon jug, how do you measure out exactly 4 gallons of water?

(The video title is wrong. It's Die Hard 3.)

Apparently, you can solve this problem using a formal specification language like [TLA+](https://en.wikipedia.org/wiki/TLA%2B). I don't know much about this topic, but it appears that a [formal specification language](https://en.wikipedia.org/wiki/Formal_specification) is much like a programming language in that it lets you describe the behavior of a system. However, it's much more rigorous and builds on mathematical techniques that enable you to reason more effectively about the behavior of the system you're describing than you can with a typical programming language. In a recent discussion on Hacker News about TLA+, I came across [this comment](https://news.ycombinator.com/item?id=13919251) which linked to a fun and simple example showing [how to solve the Die Hard 3 problem with TLA+](https://github.com/tlaplus/Examples/blob/master/specifications/DieHard/DieHard.tla). I had to watch the first two lectures from [Leslie Lamport's video course on TLA+](http://lamport.azurewebsites.net/video/videos.html) to understand the example well, but once I did I was reminded of the idea of property-based testing and, specifically, [Hypothesis](http://hypothesis.works/). So what's property-based testing? It's a powerful way of testing your logic by giving your machine a high-level description of how your code should behave and letting it generate test cases automatically to see if that description holds. Compare that to traditional unit testing, for example, where you manually code up specific inputs and outputs and make sure they match. ## How not to Die Hard with Hypothesis Hypothesis has an excellent implementation of property-based testing [for Python](https://github.com/HypothesisWorks/hypothesis-python). I thought to myself: I wonder if you can write that Die Hard specification using Hypothesis? As it turns out, Hypothesis supports [stateful testing](https://hypothesis.readthedocs.io/en/latest/stateful.html), and I was able to port the [TLA+ example](https://github.com/tlaplus/Examples/blob/master/specifications/DieHard/DieHard.tla) to Python pretty easily: ```python from hypothesis import note, settings from hypothesis.stateful import RuleBasedStateMachine, invariant, rule # The default is not always enough for Hypothesis to find a failing example. @settings(max_examples=2000) class DieHardProblem(RuleBasedStateMachine): small = 0 big = 0 @rule() def fill_small(self): self.small = 3 @rule() def fill_big(self): self.big = 5 @rule() def empty_small(self): self.small = 0 @rule() def empty_big(self): self.big = 0 @rule() def pour_small_into_big(self): old_big = self.big self.big = min(5, self.big + self.small) self.small = self.small - (self.big - old_big) @rule() def pour_big_into_small(self): old_small = self.small self.small = min(3, self.small + self.big) self.big = self.big - (self.small - old_small) @invariant() def physics_of_jugs(self): assert 0 <= self.small <= 3 assert 0 <= self.big <= 5 @invariant() def die_hard_problem_not_solved(self): note(f"> small: {self.small} big: {self.big}") assert self.big != 4 DieHardTest = DieHardProblem.TestCase ``` Calling `pytest` on this file quickly digs up a solution: ``` self = DieHardProblem({}) @invariant() def die_hard_problem_not_solved(self): note("> small: {s} big: {b}".format(s=self.small, b=self.big)) > assert self.big != 4 E AssertionError: assert 4 != 4 E + where 4 = DieHardProblem({}).big how-not-to-die-hard-with-hypothesis.py:17: AssertionError ----------------------------- Hypothesis ----------------------------- > small: 0 big: 0 Step #1: fill_big() > small: 0 big: 5 Step #2: pour_big_into_small() > small: 3 big: 2 Step #3: empty_small() > small: 0 big: 2 Step #4: pour_big_into_small() > small: 2 big: 0 Step #5: fill_big() > small: 2 big: 5 Step #6: pour_big_into_small() > small: 3 big: 4 ====================== 1 failed in 0.22 seconds ====================== ``` ## What's Going on Here The code and test output are pretty self-explanatory, but here's a recap of what's going on: We're defining a state machine. That state machine has an initial state (two empty jugs) along with some possible transitions. Those transitions are captured with the `rule()` decorator. The initial state and possible transitions together define how our system works. Next we define invariants, which are properties that must always hold true in our system. Our first invariant, `physics_of_jugs`, says that the jugs must hold an amount of water that makes sense. For example, the big jug can never hold more than 5 gallons of water. Our next invariant, `die_hard_problem_not_solved`, is where it gets interesting. Here we're declaring that the problem of getting exactly 4 gallons in the big jug _cannot_ be solved. Since Hypothesis's job is to test our logic for bugs, it will give our state machine a thorough shake down and see if we ever violate our invariants. In other words, we're basically goading Hypothesis into solving the Die Hard problem for us. I'm not entirely clear on how Hypothesis does its work, but I know the basic summary is this: It takes the program properties we've specified -- including things like rules, invariants, data types, and function signatures -- and generates data or actions to probe the behavior of our program. If Hypothesis finds a piece of data or sequence of actions that get our program to violate its stated properties, it tries to whittle that down to a _minimum falsifying example_---i.e. something that exposes the same problem but with a minimum number of steps. This makes it much easier for you to understand how Hypothesis broke your code. Hypothesis's output above tells us that it was able to violate the `die_hard_problem_not_solved` invariant and provides us with a minimal reproduction showing exactly how it did so. That reproduction is our solution to the problem. It's also how McClane and Carver did it in the movie! ## Final Thoughts All in all, I was pretty impressed with how straightforward it was to translate the TLA+ example into Python using Hypothesis. And when Hypothesis spit out the solution, I couldn't help but smile. It's pretty cool to see your computer essentially generate a program that solves a problem for you. And the Python version of the Die Hard "spec" is not much more verbose than the original in TLA+, though TLA+'s notation for current vs. next value (e.g. `small` vs. `small'`) is elegant and cuts out the need to have variables like `old_small` and `old_big`. I don't know how Hypothesis compares to TLA+ in a general sense. I've only just started to learn about property-based testing and TLA+, and I wonder if they have a place in the work that I do these days, which is mostly Data Engineering-type stuff. Still, I found this little exercise fun, and I hope you learned something interesting from it. _Thanks to [Julia], [Dan], Laura, Anjana, and Cip for reading drafts of this post._ [Julia]: http://jvns.ca/ [Dan]: https://danluu.com/ ================================================ FILE: website/content/2017-07-16-types-and-properties.md ================================================ --- tags: technical, details, python, alternatives date: 2017-07-16 10:00 title: Moving Beyond Types author: drmaciver --- If you look at the original property-based testing library, the Haskell version of QuickCheck, tests are very closely tied to types: The way you typically specify a property is by inferring the data that needs to be generated from the types the test function expects for its arguments. This is a bad idea. The first part of why [I've talked about already](../integrated-shrinking/) - you don't want to tie the shrinking of the data to its type, because it makes testing significantly more brittle. But you could solve that and still have data generation be tied to type, and it would still be a bad idea. The reason for this is very simple: Often you want to generate something much more specific than any value of a given type. If you look at the [Hypothesis strategies module](https://hypothesis.readthedocs.io/en/latest/data.html) and what you can generate, many of these look like types. But if you look closer, nearly every single one of them has options to configure them to be more specific. Consider something like the following: ```python from statistics import mean from hypothesis import given, strategies as st @given(st.lists(st.floats(allow_nan=False, allow_infinity=False), min_size=1)) def test_mean_is_in_bounds(ls): assert min(ls) <= mean(ls) <= max(ls) ``` In this test we've restricted the domain we're testing on so we can focus on a property that doesn't hold in generality: The mean of an empty list is an error, so that needs a special case test for it, and calculations with `NaN` or infinity are special cases that don't always have a well-defined result and even when they do don't necessarily satisfy this property. This happens a lot: Frequently there are properties that only hold in some restricted domain, and so you want more specific tests for that domain to complement your other tests for the larger range of data. When this happens you need tools to generate something more specific, and those requirements don't map naturally to types. Consider writing this code based on types instead: ```python from statistics import mean from hypothesis import given, strategies as st @given(ls=...) def test_mean_is_in_bounds(ls: list[float]): assert min(ls) <= mean(ls) <= max(ls) ``` (this code doesn't work at the time of this writing, but it will soon - [the pull request to implement it](https://github.com/HypothesisWorks/hypothesis-python/pull/643) is in fairly late-stage review). But this doesn't do the right thing: We've dropped the conditions from the previous test that our floats are all finite and our lists are all non-empty. So now we have to add a precondition to make the test valid: ```python import math from statistics import mean from hypothesis import assume, given, strategies as st @given(ls=...) def test_mean_is_in_bounds(ls: list[float]): assume(len(ls) > 1) assume(all(math.isfinite(x) for x in ls)) assert min(ls) <= mean(ls) <= max(ls) ``` But this is now substantially longer and less readable than the original approach! In Haskell, traditionally we would fix this with a newtype declaration which wraps the type. We could find a newtype NonEmptyList and a newtype FiniteFloat and then say that we actually wanted a NonEmptyList[FiniteFloat] there. In Python we could probably do more or less the same thing, either by creating new wrapper types or by subclassing list and float (which you shouldn't do. Subclassing builtins in Python leads to really weird behaviour) if we wanted to save a few lines, but it's much more noisy. But why should we bother? Especially if we're only using these in one test, we're not actually interested in these types at all, and it just adds a whole bunch of syntactic noise when you could just pass the data generators directly. Defining new types for the data you want to generate is purely a workaround for a limitation of the API. If you were working in a dependently typed language where you could already naturally express this in the type system it *might* be OK (I don't have any direct experience of working in type systems that strong), but I'm sceptical of being able to make it work well - you're unlikely to be able to automatically derive data generators in the general case, because the needs of data generation "go in the opposite direction" from types (a type is effectively a predicate which consumes a value, where a data generator is a function that produces a value, so in order to produce a generator for a type automatically you need to basically invert the predicate). I suspect most approaches here will leave you with a bunch of sharp edges, but I would be interested to see experiments in this direction. That being said, I don't think it's that useful to do. Where stronger types are naturally present in the program, taking advantage of them to get better testing is certainly worth doing, but creating a specific type to represent the exact range of valid inputs to a specific test isn't because for tests the impact of a run time error that you could have caught at compile time is substantially reduced (all it causes is a spurious test failure - where your tests are long running or non-deterministic as in property-based testing this *does* matter, but it still doesn't cause a problem in production). But this is creating hard problems where none need exist, and beside which it's not the situation with most of the current generation of languages and property based libraries for them. Here, using explicit data generators is a clear improvement, and just as well-typed (in a statically typed language each data generator has a return type after all). You *can* use data generators directly in Haskell QuickCheck too, with an explicit [forAll](https://hackage.haskell.org/package/QuickCheck-2.10.0.1/docs/Test-QuickCheck-Property.html#v:forAll) but it's almost as awkward as the newtype approach, particularly if you want more than one data generator (it's even more awkward if you want shrinking - you have to use forAllWithShrink and explicitly pass a shrink function). This is more or less intrinsic to the type based approach. If you want tinkering with the data generation to be non-awkward, starting from data generators needs to become the default. And experience suggests that when you make customising the data generation easy, people do it. It's nice to be able to be more specific in your testing, but if you make it too high effort people either don't do it, or do it using less effective methods like adding preconditions to tests (assume in Hypothesis, or `==>` in QuickCheck), either of which reduces the quality of your testing and causes more bugs to slip through as a result. Fortunately, it already *is* the default in most of the newer implementations of property-based testing. The only holdouts are ones that directly copied Haskell QuickCheck. Originally this was making a virtue of a necessity - most of the implementations which started off the trend of data generator first tests are either for dynamic languages (e.g. Erlang, Clojure, Python) or languages with very weak type systems (e.g. C) where type first is more or less impossible, but it's proven to be a much more usable design. And it's perfectly compatible with static typing too. [Hedgehog](https://hackage.haskell.org/package/hedgehog) is a relatively recent property-based testing library for Haskell that takes this approach, and it works just as well in Haskell as it does in any language. It's also perfectly compatible with being able to derive a data generator from a type for the cases where you really want to. We saw a hint at that with the upcoming Hypothesis implementation above. You could easily do the same by having something like the following in Haskell (mimicking the type class of QuickCheck): ```haskell class Arbitrary a where arbitrary :: Gen a ``` You can then simply use `arbitrary` like you would any other data generator. As far as I know Hedgehog doesn't do this anywhere (but you can use QuickCheck's Arbitrary with the hedgehog-quickcheck package), but in principle there's nothing stopping it. Having this also makes it much easier to define new data generators. I'm unlikely to use the support for `@given` much, but I'm much more excited that it will also work with `builds`, which will allow for a fairly seamless transition between inferring the default strategy for a type and writing a custom generator. You will, for example, be able to do `builds(MyType)` and have every constructor argument automatically filled in (if it's suitably annotated), but you can also do e.g. `builds(MyType, some_field=some_generator)` to override a particular default while leaving the others alone. (This API is somewhere where the dynamic nature of Python helps a fair bit, but you could almost certainly do something equivalent in Haskell with a bit more noise or a bit more template Haskell) So this approach doesn't have to be data generator-only, even if it's data generator first, but if you're going to pick one the flexibility of the data generator based test specification is hard to beat, regardless of how good your type system is. ================================================ FILE: website/content/2017-09-14-multi-bug-discovery.md ================================================ --- tags: technical, details, python date: 2017-09-26 12:00 title: When multiple bugs attack author: drmaciver --- When Hypothesis finds an example triggering a bug, it tries to shrink the example down to something simpler that triggers it. This is a pretty common feature, and most property-based testing libraries implement something similar (though there are a number of differences between them). Stand-alone test case reducers are also fairly common, as it's a useful thing to be able to do when reporting bugs in external projects - rather than submitting a giant file triggering the bug, a good test case reducer can often shrink it down to a couple of lines. But there's a problem with doing this: How do you know that the bug you started with is the same as the bug you ended up with? This isn't just an academic question. [It's very common for the bug you started with to slip to another one](https://blog.regehr.org/archives/1284). Consider for example, the following test: ```python from hypothesis import given, strategies as st def mean(ls): return sum(ls) / len(ls) @given(st.lists(st.floats())) def test(ls): assert min(ls) <= mean(ls) <= max(ls) ``` This has a number of interesting ways to fail: We could pass `NaN`, we could pass `[-float('inf'), +float('inf')]`, we could pass numbers which trigger a precision error, etc. But after test case reduction, we'll pass the empty list and it will fail because we tried to take the min of an empty sequence. This isn't necessarily a huge problem - we're still finding a bug after all (though in this case as much in the test as in the code under test) - and sometimes it's even desirable - you find more bugs this way, and sometimes they're ones that Hypothesis would have missed - but often it's not, and an interesting and rare bug slips to a boring and common one. Historically Hypothesis has had a better answer to this than most - because of the Hypothesis example database, all intermediate bugs are saved and a selection of them will be replayed when you rerun the test. So if you fix one bug then rerun the test, you'll find the other bugs that were previously being hidden from you by that simpler bug. But that's still not a great user experience - it means that you're not getting nearly as much information as you could be, and you're fixing bugs in Hypothesis's priority order rather than yours. Wouldn't it be better if Hypothesis just told you about all of the bugs it found and you could prioritise them yourself? Well, as of Hypothesis 3.29.0, released a few weeks ago, now it does! If you run the above test now, you'll get the following: ``` Falsifying example: test(ls=[nan]) Traceback (most recent call last): File "/home/david/hypothesis-python/src/hypothesis/core.py", line 671, in run print_example=True, is_final=True File "/home/david/hypothesis-python/src/hypothesis/executors.py", line 58, in default_new_style_executor return function(data) File "/home/david/hypothesis-python/src/hypothesis/core.py", line 120, in run return test(*args, **kwargs) File "broken.py", line 8, in test def test(ls): File "/home/david/hypothesis-python/src/hypothesis/core.py", line 531, in timed_test result = test(*args, **kwargs) File "broken.py", line 9, in test assert min(ls) <= mean(ls) <= max(ls) AssertionError Falsifying example: test(ls=[]) Traceback (most recent call last): File "/home/david/hypothesis-python/src/hypothesis/core.py", line 671, in run print_example=True, is_final=True File "/home/david/hypothesis-python/src/hypothesis/executors.py", line 58, in default_new_style_executor return function(data) File "/home/david/hypothesis-python/src/hypothesis/core.py", line 120, in run return test(*args, **kwargs) File "broken.py", line 8, in test def test(ls): File "/home/david/hypothesis-python/src/hypothesis/core.py", line 531, in timed_test result = test(*args, **kwargs) File "broken.py", line 9, in test assert min(ls) <= mean(ls) <= max(ls) ValueError: min() arg is an empty sequence You can add @seed(67388524433957857561882369659879357765) to this test to reproduce this failure. Traceback (most recent call last): File "broken.py", line 12, in test() File "broken.py", line 8, in test def test(ls): File "/home/david/hypothesis-python/src/hypothesis/core.py", line 815, in wrapped_test state.run() File "/home/david/hypothesis-python/src/hypothesis/core.py", line 732, in run len(self.falsifying_examples,))) hypothesis.errors.MultipleFailures: Hypothesis found 2 distinct failures. ``` (The stack traces are a bit noisy, I know. [We have an issue open about cleaning them up](https://github.com/HypothesisWorks/hypothesis-python/issues/848)). All of the different bugs are minimized simultaneously and take full advantage of Hypothesis's example shrinking, so each bug is as easy (or hard) to read as if it were the only bug we'd found. This isn't perfect: The heuristic we use for determining if two bugs are the same is whether they have the same exception type and the exception is thrown from the same line. This will necessarily conflate some bugs that are actually different - for example, `[float('nan')]`, `[-float('inf'), float('inf')]` and `[3002399751580415.0, 3002399751580415.0, 3002399751580415.0]` each trigger the assertion in the test, but they are arguably "different" bugs. But that's OK. The heuristic is deliberately conservative - the point is not that it can distinguish whether any two examples are the same bug, just that any two examples it distinguishes are different enough that it's interesting to show both, and this heuristic definitely manages that. As far as I know this is a first in property-based testing libraries (though something like it is common in fuzzing tools, and [theft is hot on our tail with something similar]( https://github.com/silentbicycle/theft/compare/develop-failure_tagging)) and there's been [some interesting related but mostly orthogonal research]( http://www.cse.chalmers.se/~nicsma/papers/more-bugs.pdf) in Erlang QuickCheck. It was also surprisingly easy. A lot of things went right in writing this feature, some of them technical, some of them social, somewhere in between. The technical ones are fairly straightforward: Hypothesis's core model turned out to be very well suited to this feature. Because Hypothesis has a single unified intermediate representation which defines a total ordering for simplicity, adapting Hypothesis to shrink multiple things at once was quite easy - whenever we attempt a shrink and it produces a different bug than the one we were looking for, we compare it to our existing best example for that bug and replace it if the current one is better (or we've discovered a new bug). We then just repeatedly run the shrinking process for each bug we know about until they've all been fully shrunk. This is in a sense not surprising - I've been thinking about the problem of multiple-shrinking for a long time and, while this is the first time it's actually appeared in Hypothesis, the current choice of model was very much informed by it. The social ones are perhaps more interesting. Certainly I'm very pleased with how they turned out here. The first is that this work emerged tangentially from [the recent Stripe funded work](https://stripe.com/blog/hypothesis) - Stripe paid me to develop some initial support for testing Pandas code with Hypothesis, and I observed a bunch of bug slippage happening in the wild while I was testing that (it turns out there are quite a lot of ways to trigger exceptions from Pandas - they weren't really Pandas bugs so much as bugs in the Pandas integration, but they still slipped between several different exception types), so that was what got me thinking about this problem again. Not by accident, this feature also greatly simplified the implementation of [the new deadline feature](https://hypothesis.readthedocs.io/en/latest/settings.html#hypothesis.settings.deadline) that [Smarkets](https://smarkets.com/) funded, which was going to have to have a lot of logic about how deadlines and bugs interacted, but all that went away as soon as we were able to handle multiple bugs sensibly. This has been a relatively consistent theme in Hypothesis development - practical problems tend to spark related interesting theoretical developments. It's not a huge exaggeration to say that the fundamental Hypothesis model exists because I wanted to support testing Django nicely. So the recent funded development from Stripe and Smarkets has been a great way to spark a lot of seemingly unrelated development and improve Hypothesis for everyone, even outside the scope of the funded work. Another thing that really helped here is our review process, and [the review from Zac in particular](https://github.com/HypothesisWorks/hypothesis-python/pull/836). This wasn't the feature I originally set out to develop. It started out life as a much simpler feature that used much of the same machinery, and just had a goal of avoiding slipping to new errors all together. Zac pushed back with some good questions around whether this was really the correct thing to do, and after some experimentation and feedback I eventually hit on the design that lead to displaying all of the errors. Our [review handbook](https://github.com/HypothesisWorks/hypothesis-python/blob/master/guides/review.rst) emphasises that code review is a collaborative design process, and I feel this was a particularly good example of that. We've created a great culture of code review, and we're reaping the benefits (and if you want to get in on it, we could always use more people able and willing to do review...). All told, I'm really pleased with how this turned out. I think it's a nice example of getting a lot of things right up front and this resulting in a really cool new feature. I'm looking forward to seeing how it behaves in the wild. If you notice any particularly fun examples, do [let me know](mailto:david@drmaciver.com), or write up a post about them yourself! ================================================ FILE: website/content/2017-09-28-threshold-problem.md ================================================ --- tags: technical, details, python date: 2017-09-28 11:00 title: The Threshold Problem author: drmaciver --- In [my last post](../multi-bug-discovery/) I mentioned the problem of bug slippage: When you start with one bug, reduce the test case, and end up with another bug. I've run into another related problem twice now, and it's not one I've seen talked about previously. The problem is this: Sometimes shrinking makes a bug seem much less interesting than it actually is. I first noticed this problem when [Ned Batchelder](https://nedbatchelder.com/) asked me about some confusing behaviour he was seeing: He was testing some floating point code and had an assertion that the error was not greater than some threshold. Let's say 0.5 (I could dig up the IRC logs, but the exact number doesn't matter). Hypothesis said "Ah ha! Here is an example where the error is 0.500001. A bug!". Ned sighed and thought "Oh great, floating point precision issues", but on further investigation it turned out that that wasn't it at all. The error could be arbitrarily large, it's just that Hypothesis reliably gave an example where it was almost as small as it could possibly be and still fail. This wasn't a bug, either. This is how Hypothesis, QuickCheck, and all of the other tools in this family are designed to work. The problem is that test case reduction is designed to produce the simplest example possible to demonstrate the bug. If the bug can be expressed as happening when some score exceeds some threshold, and the score is one that tends to increase with example size, then the failing example that a property-based testing library gives you will tend to be one where the score is barely above that threshold, making the problem look much less bad than it actually is. This isn't even a bug in Hypothesis - QuickCheck or any other property-based testing would do the same. It's literally working as intended. Arguably it's not even really a problem: Hypothesis has demonstrated the bug, and it's done so with a simple example which should thus be easy to understand. But I can't help but feel that we could do better. It definitely produces misleading examples even if they technically demonstrate the right problem, and misleading examples are a great way to waste the user's time. I also ran into this problem again recently, where it was more of a problem because it was resulting in flaky tests. I recently introduced [a deadline feature](https://hypothesis.readthedocs.io/en/latest/settings.html?highlight=deadline#hypothesis.settings.deadline) as part of the work on Hypothesis performance legibility that [Smarkets](https://smarkets.com/) are funding. This causes slow examples to be treated as failures: If an example passes but took longer than your deadline to run, it raises `DeadlineExceeded`. This is treated as a normal error and Hypothesis shrinks it like anything else (including allowing it to participate in the multi-shrinking process). The problem is that it's exactly this sort of threshold problem: You literally have a score (the run time) and a threshold (the deadline) such that when the score exceeds the threshold the test fails. Large examples are certainly likely to be slower, so you will consistently get examples which are right on the boundary of being too slow. Which is fine, except that Hypothesis relies on repeatability to display test errors - once it has a minimized example, it replays the test so it can show you the example, print the exception, etc. And test run times are not actually repeatable - a test that takes 201ms on first running might take 199ms on the next run. This then results in Hypothesis thinking the test is flaky - it previously raised `DeadlineExceeded`, and now it doesn't. This lead to [Issue 892](http://github.com/HypothesisWorks/hypothesis-python/issues/892), where Florian Bruhin ran into precisely this problem when testing [Qutebrowser](https://www.qutebrowser.org/). The [solution I've ended up opting for there](https://github.com/HypothesisWorks/hypothesis-python/pull/899) is to temporarily raise the deadline during shrinking to something halfway between the actual deadline and the largest runtime we've seen. This ensures that we shrink to a larger threshold than the deadline, and then when we replay we should comfortably exceed the real deadline unless the test performance actually *is* really flaky (in which case I've also improved the error message). This solution is currently very specific to the problem of the deadlines, and that's fine - there's no need to rush to a fully general solution, and deadlines have slightly different constraints than other variants of this due to the unreliability of timing - but it is something I'd like to see solved more generally. One thing I have thought about for a while is adding some notion of scoring to Hypothesis - e.g. letting people record some score that recorded your progress in testing (testing games where the score could be e.g. the level you've reached, or your literal score in the game, was one use case I had in mind). This would seem to be another good example for that - if you could make your score available to Hypothesis in some way (or if Hypothesis could figure it out automatically!), then a similar solution to the above could be used: If Hypothesis notices that the score of the shrunk example is drastically different from the score of the starting example, it could try rerunning the shrinking process with the additional constraint that the score should stay closer to that of the original example, and display the newly shrunk example with the larger (or smaller) score alongside it. This would work as part of the new multiple failures reporting, so you would see both examples side by side. This needs more thought before I jump in and implement something, but I think this is an important problem to solve to improve the usability of Hypothesis in particular and property-based testing in general. Shrinking is a great start to making the problems testing exposes legible to users, but it's only a start, and we need to do more to try to improve developers' productivity when debugging the problems we show them. ================================================ FILE: website/content/2018-01-08-smarkets.md ================================================ --- tags: python date: 2018-01-08 06:00 title: Smarkets's funding of Hypothesis author: drmaciver --- Happy new year everybody! In this post I'd like to tell you about one of the nice things that happened in 2017: The Hypothesis work that was funded by [Smarkets](https://smarkets.com/careers). Smarkets are an exchange for peer-to-peer trading of bets but, more importantly for us, they are fairly heavy users of Hypothesis for the Python part of their stack. Smarkets approached me a while back to talk about possibly funding some Hypothesis development. We talked a bit about what their biggest pain points with Hypothesis were, and it emerged that they were having a lot of trouble with Hypothesis performance. We talked a bit about how to speed Hypothesis up, and I suggested some diagnostic tests they could do to see where the problem was, and it fairly quickly emerged that in fact they mostly *weren't* having trouble with Hypothesis performance - Hypothesis indeed was much slower than it should have been in their use case, but more than an order of magnitude more time was spent in their test code rather than in Hypothesis. So on further discussion we decided that actually their big problem was not the performance of Hypothesis per se, but instead the *legibility* of Hypothesis performance problems - when tests using Hypothesis were slow, it was non-obvious why that might be the case, and it might be extremely difficult to reproduce the problem. This is the sort of problem where it's really useful to have user feedback and funding, because it's more or less a non-problem for me and - to a lesser extent - anyone who already works on Hypothesis. Because we've got much deeper knowledge of the internals and the failure modes, we're largely just used to working around these issues. More feedback from Hypothesis would be *helpful*, but it's not *essential*. So, given that in the normal course of things Hypothesis development is mostly driven by what we feel like working on, this is really the sort of work that will only happen with a source of external funding for it. Thus it's really great that Smarkets were willing to step up and fund it! After some discussion we ended up settling on four features that would significantly improve the situation for them: ### Identification of examples causing slow tests This was the introduction of the [deadline](https://hypothesis.readthedocs.io/en/latest/settings.html#hypothesis.settings.deadline) feature, which causes Hypothesis to treat slow tests as failures - when set, a test that takes longer than its set deadline (not counting data generation) raises a `DeadlineExceeded` error. This is a bit of a blunt instrument, but it is *very* effective at getting test runtime under control! There turned out to be some amusing complications in developing it, so this feature was spread over a number of releases as we found and worked out its various kinks (3.27.0, 3.31.1, 3.38.2). One of the interesting problems we found was that deadlines have a [threshold problem](../threshold-problem/) - because the shrinking process tends to find examples which are just on the cusp of failure, often when you rerun a fully shrunk example it doesn't fail! I went back and forth on the best solution for this for a while, but in the end the best solution turned out to be a simple one - raise the deadline during example generation and shrinking, then replay with the actual set deadline. This does mean that tests that are *right* on the cusp of being too slow may pass artificially, but that's substantially better than introducing flaky failures. On top of this we also needed to make this feature play well with [inline data generation](https://hypothesis.readthedocs.io/en/latest/data.html#drawing-interactively-in-tests) - the complicating factor was that data generation is much faster when replaying examples or shrinking than it is during generation (the generation logic is rather too complicated. I have some long-term plans to make it simpler, which would make this difference largely go away). Fortunately, the work done for the next feature made this easy to do. ### Breakdown of generation time in statistics This was a fairly simple change, prompted by the initial confusion we had in diagnosing Smarket's test problems: If you can't tell the difference between slow data generation and slow tests, your initial guesses about performance may be very misleading! So this change updated [the statistics reporting system](https://hypothesis.readthedocs.io/en/latest/details.html#statistics) to report what fraction of time is spent in data generation. If you run tests with statistics you'll now see a line like the following: ``` - Fraction of time spent in data generation: ~ 12% ``` A small change, but a very helpful one! This came in in Hypothesis 3.38.4. ### Health check overhaul Hypothesis has had a health check system for a while. Its general goal is to suggest that you might not want to do that thing you're doing - roughly analogous to compiler warnings. It's useful for guiding you into correct use of Hypothesis and helping you avoid things that might degrade the quality of your testing. It's historically had some problems: In particular the main source of health checks did not actually run your tests! They just run the data generation with the test stubbed out. This meant that you could very easily accidentally bypass the health checks by e.g. using inline data generation, or doing your filtering with assume. It also had the problem that it wasn't running the real data generation algorithm but instead an approximation to it. This meant that things that would work fine in practice would sometimes fail health checks. This piece of work was an overhaul of the health check system to solve these problems and to expand the scope of the problems it could find. It ended up working very well. So well in fact that it found some problems in Hypothesis's built in library of strategies! It was split across a number of releases: * In 3.37.0 we deprecated a number of existing health checks that no longer did anything useful. * In 3.38.0 I overhauled the health check system to be based on actual test execution, solving the existing limitations of it. * In 3.39.0 I added a new health check that tests whether the smallest example of the test was too large to allow reasonable testing - accidentally generating very large examples being a common source of performance bugs. The health check in 3.39.0 turned out to catch a major problem in Hypothesis's handling of blacklist\_characters and some regular expression constructs, so prior to that we had to release 3.38.8 to fix those! Over all I'm much happier with the new health check system and think it does a much better job of shaping user behaviour to get better results out of Hypothesis. ### Printing reproduction steps Historically output from Hypothesis has looked something like this: ``` Falsifying example: test_is_minimal(ls=[0], v=1) ``` Or, if you had a failed health check, like the following: ``` hypothesis.errors.FailedHealthCheck: It looks like your strategy is filtering out a lot of data. Health check found 50 filtered examples but only 0 good ones. This will make your tests much slower, and also will probably distort the data generation quite a lot. You should adapt your strategy to filter less. This can also be caused by a low max_leaves parameter in recursive() calls. See https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck for more information about this. If you want to disable just this health check, add HealthCheck.filter_too_much to the suppress_health_check settings for this test. ``` This is fine if you're running the tests locally, but if your failure is on CI this can be difficult to reproduce. If you got a falsifying example, you're only able to reproduce it if all of your arguments have sensible reprs (which may not be the case even if you restrict yourself to Hypothesis's built in strategies - e.g. using inline data generation prevents it!). If you got a health check failure, there's nothing that helps you reproduce it at all! So the proposed feature for this was to print out the random seed that produced this: ``` You can add @seed(302934307671667531413257853548643485645) to this test or run pytest with --hypothesis-seed=302934307671667531413257853548643485645 to reproduce this failure. ``` This was a great idea, and seemed to work out pretty well when we introduced it in 3.30.0, but on heavier use in the wild turned out to have some fairly major problems! The big issue is that in order to reproduce Hypothesis's behaviour on a given run you need to know not just the random seed that got you there, but also the state of Hypothesis's example database! Hypothesis [maintains a cache of many of the previously run examples](https://hypothesis.readthedocs.io/en/latest/database.html), and uses it to inform the testing by replaying test cases that e.g. failed the last time they were run, or covered some hard to reach line in the code. Even for examples that don't come from the database, Hypothesis under the hood is a mutation based fuzzer, so all the examples it finds will depend on the examples it loaded. The initial solution to this (3.40.0) was just to turn off seed printing when its output would be misleading. This worked, but was fairly non-ideal even just for Smarkets - they *do* use the database in their CI, so this would result in a lot of failures to print. After some discussion, I decided that given that the feature wasn't nearly as useful as intended, so I threw in an extra freebie feature to make up the gap in functionality: [@reproduce\_failure](https://hypothesis.readthedocs.io/en/latest/reproducing.html#reproducing-an-example-with-with-reproduce-failure). This uses Hypothesis's internal format to replicate the functionality of the database in a way that is easy to copy and paste into your code. It took some careful designing to make it usable - my big concern was that people would leave this in their code, blocking future upgrades to Hypothesis - but in the end I'm *reasonably* happy with the result. As a bonus, the work here allowed me to sort out one big concern about seed printing: We still needed a way to reproduce health check failures when the database was being used. The solution was in the end easy: We just don't use the examples from the database in the part where the main health checks are running. This still leaves a few health checks which could theoretically be hard to reproduce (the main one is the hung test health check, but that one tends to reproduce fairly reliably on any seed if you have deadlines on). So this leaves us with a state where health check failures will suggest `@seed` and example failures will suggest `@reproduce_failure` (where necessary. The linked documentation spells this out in more detail). ### Ten releases later In the end the Smarkets work came to a total of exactly 10 releases, some larger than other. The end result has been very beneficial, and not just to Smarkets! I've had several users report back improvements to their tests as a result of the new health checks, and I've personally found the `@reproduce_failure` feature remarkably useful. I'm very happy to have done this work, and am grateful to Smarkets for funding it. I think this sort of thing where commercial users fund the "boring" features that are very useful for people using the tool at scale but maintainers are unlikely to work on under their own initiative is a very good one, and I hope we'll do more of it in future. As a bonus, Smarkets kindly agreed to put online the talk I gave them about Hypothesis (largely intended to raise awareness of it and property-based testing among their teams who aren't using it yet). If you want to learn more about some of the philosophy and practice behind using this sort of testing, or want something to send to people who aren't convinced yet, you can watch it [here](https://smarketshq.com/a-talk-on-hypothesis-e7182b95ced1). ================================================ FILE: website/content/2018-02-27-continuous-releases.md ================================================ --- tags: development-process date: 2018-02-27 07:00 title: The Hypothesis continuous release process author: alexwlchan --- If you watch [the Hypothesis changelog][changelog], you'll notice the rate of releases sped up dramatically in 2017. We released over a hundred different versions, sometimes multiple times a day. This is all thanks to our continuous release process. We've completely automated the process of releasing, so every pull request that changes code gets a new release, without any human input. In this post, I'll explain how our continuous releases work, and why we find it so useful. [changelog]: https://hypothesis.readthedocs.io/en/latest/changelog.html ## How it works In the past, Hypothesis was released manually. Somebody had to write a changelog, tag a new release on GitHub, and run some manual pip commands to publish a new version to PyPI -- and only David had the credentials for the latter. This meant that releases were infrequent, and features spent a long time in master before they were available to `pip install`. The pace of development picked up in 2017 -- partly as new maintainers arrived, and partly groundwork for [David's upcoming (now started) PhD][phd] -- and we wanted to be able to release more frequently. We decided to automate the entire release process. Now, when you create a pull request that changes the Hypothesis code -- anything that gets installed by pip -- you have to include a `RELEASE.rst` file which describes your change. Here's an example from [a recent pull request][recent]: RELEASE_TYPE: patch This release changes the way in which Hypothesis tries to shrink the size of examples. It probably won't have much impact, but might make shrinking faster in some cases. It is unlikely but not impossible that it will change the resulting examples. The first line says whether this is a major, minor, or patch release (using [semantic versioning][semver]). The rest is a description of the changes in your patch. We have a test in CI that checks for this file -- any change to the core code needs a release file, even [fixing a typo][typo]. If you need a release file but haven't written one, the tests fail and your pull request won't be merged. Sometimes we write a release file even if there aren't changes to the core code, but we think it's worth a release anyway. For example, changes to the installation code in `setup.py`, or larger changes to our test code for the benefit of downstream packagers. Once you've written a release file and the pull request is merged into master, and after all the other tests have passed, our CI uses this file to create a new release. First, it works out the new version number, and updates it in [version.py][version.py]. Then it copies the release description into the changelog, including the new version number and the current date. For example: -------------------- 3.44.25 - 2018-02-05 -------------------- This release changes the way in which Hypothesis tries to shrink the size of examples. It probably won't have much impact, but might make shrinking faster in some cases. It is unlikely but not impossible that it will change the resulting examples. These two changes are saved as a new commit, and that commit gets tagged as the new release. The tag and the commit are pushed to GitHub, and then CI builds a new package and publishes it to PyPI. So with no very little extra work, every code change triggers a new release, and it's usually available within half an hour of merging the pull request. This exact system might not scale to larger teams. In particular, you can't merge new features until the code in master has been released -- you get conflicts around `RELEASE.rst` -- so you can only merge one pull request at a time. And in Hypothesis, we never backport bugfixes to old major or minor releases -- you'd need some changes to support that. But Hypothesis only has one full-time contributor, and everybody else works on it in their free time, we don't create patches fast enough for this to be a problem. For us, it works exceptionally well. [phd]: http://www.drmaciver.com/2017/04/life-changes-announcement-academia-edition/ [recent]: https://github.com/HypothesisWorks/hypothesis-python/pull/1101 [semver]: https://semver.org/ [typo]: https://github.com/HypothesisWorks/hypothesis-python/pull/1069 [version.py]: https://github.com/HypothesisWorks/hypothesis-python/blob/master/src/hypothesis/version.py ## Why bother? Moving to continuous releases has been amazing. The big benefit is that nobody has to do manual releases any more. Before we had this system, changelogs had to be assembled and written by hand, which meant reading the commit log since the last release. This is both boring and prone to error -- in the past, a release might contain multiple changes, and it was easy to overlook or forget something in the changelog. No more! Another benefit is that our releases happen much more quickly. Every patch is available as soon as our tests confirm it's okay, not when somebody remembers to do a release. If something's been merged, it's either available for download, or it will be very shortly. Releasing more often means each individual release is much smaller, which makes it much easier to find the source of bugs or regressions. If somebody finds a bug, we can trace it to a specific release (and corresponding pull request), and there's a relatively small amount of code to inspect. Automation also makes our release process more reliable. Manual steps have scope for error, and we've had a few dodgy releases in the past. This process has cut over 100 releases near flawlessly. Finally, every contributor gets to make a release. If you submit a patch that gets accepted, your change is available immediately, and it's entirely your work. This may less of tangible benefit, but it gives off nice fuzzy feelings, especially if it's your first patch. (Speaking of which, we're always looking [for new contributors][contributors]!) [contributors]: https://github.com/HypothesisWorks/hypothesis-python/blob/master/CONTRIBUTING.rst ## I'm ruined for everything else I've become used to code being available almost immediately after it's merged into master -- which isn't true for the vast majority of projects. When I go to a repo with a bug report, see that a bugfix was merged two weeks ago, but there's yet to be a new release, it's hard not to feel a little impatient. I've started using this in my other repos -- both these scripts exactly, and derivatives of the same idea. If you'd like to try this yourself (and I'd really encourage you to do so!), all the scripts for this process are under the same MPL license as Hypothesis itself. Look in the [scripts directory][scripts] of the main repo. In particular, `check-release-file.py` looks for a release note on pull requests, and `deploy.py` is what actually cuts the release. The code will probably need tweaking for your repo (it's closely based on the Hypothesis repo), but hopefully it provides a useful starting point. [scripts]: https://github.com/HypothesisWorks/hypothesis-python/tree/master/scripts ================================================ FILE: website/content/2020-06-08-complex-data-strategies.md ================================================ --- tags: python intro technical properties date: 2020-06-08 15:00 title: Property Testing with Complex Inputs author: hwayne --- Once you learn the basics, there are two hard parts to using property-based testing: * What are good properties to test? * How do I generate complex inputs? These are also the _main_ parts of property-based testing! And both require some skill and creativity to solve. This post is to help you get some basics on the second question, how we generate complex inputs. Often we're working with data that has lots of preconditions and we want to actually generate inputs that satisfies the preconditions. We also often need to create data that depends on other data, or data with extra conditions, or [independent-but-similar](https://www.hillelwayne.com/post/metamorphic-testing/) data. We need some way to build more complex strategies. There are a few different ways to do this, so let's go into each of them. ## The problem We have an Exam which consists of a set of questions and multiple-choice answers. Students take ExamInstances, which consists of the exam they took and the answers they gave. For each question they may either list an answer or leave it blank. There are many things we could test here, but the first step is actually generating the data. For the purposes of this tutorial we will make the data classes as simple as possible: ```py from dataclasses import dataclass @dataclass class Exam: """The abstract exam. Students take ExamInstances.""" name: str answer_key: list[int] @dataclass class ExamInstance: """The instantiation of the exam.""" student: int exam: Exam answers: list[int | None] # must be same length, but answers may be blank ``` For our property tests we need to generate both Exams and ExamInstances. We have some restrictions on the Exam, and we need the ExamInstances to match the Exam in terms of answer length. This makes it somewhat more complex than the usual "generate an integer" case. ## Generating Exams ### Type inference We don't _have to_ write a special generator for Exam. We've already type-annotated every field of Exam, so Hypothesis is smart enough to generate valid Exams via the `from_type` strategy. ```py from hypothesis import given import hypothesis.strategies as st @given(st.from_type(Exam)) def test_1(exam): ... ``` In fact, it can even generate valid ExamInstances, as all those fields are typed, too: ```py @given(st.from_type(ExamInstance)) def test_2(ei): ... ``` Sometimes this is good enough. In our case it is not for a couple of reasons: * The state space is huge. Hypothesis is using the default generators for each of the fields, when for our purposes they should be more constrained. The more narrowly we can constrain the search space the more likely we are to get interesting edge cases. * Hypothesis is going to generate a lot of "nonsensical" exams. You might get something where the answer key is `[-45, 0, 800002]`. We want to use stricter strategies for the fields of Exam. ### Simple customizations If our requirements are simple and we know that most generated inputs will be correct, we can usually get away with using `filter` and `assume`. `filter` is a method on all strategies, while `assume` goes in the body of the test itself. Both of them, when false, tell hypothesis to discard the invalid data and make a new draw. We can also use `assume` to relate several parameters to each other. ```py # very inefficient - don't do this! @given(st.from_type(Exam).filter(lambda x: x.answer_key and min(x.answer_key) >= 0)) def test_2(exam): assume(max(exam.answer_key) <= 5) ... ``` The upside of `filter` and `assume` is that they're easy to write and clearly express what our constraints are. The downside is that we are still generating bad inputs- we're just throwing it away right after. This makes the testing slower and Hypothesis might not find enough valid inputs. We're better off writing a customized generator that _always_ generates good inputs. Fortunately that's a lot easier than it sounds. ### `builds` The `builds` strategy takes in an object and a set of initialization strategies, draws the corresponding values, and passes them into the object's `__init__`. Let's say we want to make sure that `answer_key` only uses numbers between 1 and 5. We can use `builds` to do this. ```py @given(st.builds(Exam, answer_key=st.lists(st.integers(1, 5)))) def test_3(exam): for x in exam.answer_key: assert x in range(6) ``` We haven't specified what `name` is, so Hypothesis will use the default generator for text. If we want to always use the same name, we can use the `just` strategy: ```py @given(st.builds(Exam, name=st.just(""), answer_key=st.lists(st.integers(1, 5)))) def test_3(exam): # same ``` At this point we should probably pull it into its own function: ```py def exam_strategy(names=st.just(""), n_options=5): return st.builds( Exam, name=names, answer_key=st.lists(st.integers(1, n_options)), ) @given(exam_strategy()) def test_3(exam): # same ``` ### `register_type_strategy` Now that we have a custom builder, we can tell Hypothesis to always use it when inferring Exams. We do this with `register_type_strategy`. ```py def exam_strategy(): ... st.register_type_strategy(Exam, exam_strategy()) ``` This will now be used any time Hypothesis infers that it needs to build an Exam, even if it needs to do so as part of generating something else, like an ExamInstance. ```py @given(st.from_type(Exam)) def test_4(exam): ... ``` That takes care of the Exam. But we still have a problem with the ExamInstance: its `answer` must have the same length as its exam's `answer_key`. But there's no way to guarantee that with our builder. We can't link strategies to each other. ```py # This will fail @given(st.from_type(ExamInstance)) def test_5(ei): assert len(ei.answers) == len(ei.exam.answer_key) ``` In order to combine multiple strategies we need to use something a little more powerful: `composite`. ## `composite` Composite strategies are user-written functions "lifted" into full strategies. They can be a little bit unintuitive at first, so let's start with a simple example. This composite strategy returns a list of numbers where the first element of the list is always the smallest number in the list: ```py @st.composite def example(draw): i = draw(st.integers()) l = draw(st.lists(st.integers(min_value=i))) return [i] + l ``` Let's break down what's going on here. First we have the inner function. **The inner function does not return a strategy.** It returns regular Python values. The `@composite` decorator is what converts this function into a strategy. In addition to its usual parameters, the inner function also has a `draw` parameter. Calling this function on a strategy draws a concrete value. We need this because, again, the inner function only returns values, not strategies. We do not explicitly include `draw` when calling the full function; the decorator takes care of that. So we would call this function in a test like: ```py @given(example()) def test_example(l): assert min(l) == l[0] ``` We can do anything we want inside the body of the composite function, making it an exceptionally powerful tool. Here's how we can make sure that our Exam and ExamInstance have the same length: ```py # Helper function to make the following examples terser def ei_answers_strategy(exam): return st.lists( st.none() | st.integers(1, 5), min_size=len(exam.answer_key), max_size=len(exam.answer_key), ) @st.composite def exam_instance_strategy(draw): exam = draw(st.from_type(Exam)) answers = ei_answers_strategy(exam) return ExamInstance(student=1, exam=exam, answers=draw(answers)) ``` We use it like any other strategy. ```py @given(exam_instance_strategy()) def test_6(ei): assert len(ei.answers) == len(ei.exam.answer_key) ``` ### More with `composite` Composite strategies are just functions and can be called in other strategies. This means you can use it to create complex connected data. Let's say we want to generate several `ExamInstances` for the same student and exam. We can break this into several composable pieces: ```py @st.composite def exam_instance_for_exam(draw, exam, student=""): answers = ei_answers_strategy(exam) return ExamInstance(student=student, exam=exam, answers=draw(answers)) @st.composite def many_exam_instances(draw, student=""): exam = draw(st.from_type(Exam)) exams = st.lists(exam_instance_for_exam(exam, student), min_size=1) return draw(exams) @given(many_exam_instances("brian")) def test_7(eis): ... ``` In this case we're only passing raw values into the composite. This means we can't randomize the name of the student. In some cases this is an advantage, as it restricts the state space we have to search. In other cases this is a disadvantage, as it goes against the spirit of property testing. If you want the more thorough testing, you can pass in strategies instead into the composite and draw values in the body of the function. ```py @st.composite def exam_instance_for_exam(draw, exam, student=st.just("")): student = draw(student) exam = draw(exam) answers = ei_answers_strategy(exam) return ExamInstance(student=student, exam=exam, answers=draw(answers)) # many_exam_instances is the same... for now ``` There's a usability problem with this, though. Imagine we have `many_exam_instances` pass in complex strategies for `exam` and `student`. How do we know what values we drew? In this _particular_ case we can extract them from the returned ExamInstance, but we can't always rely on that, especially if we do more complex transformations. For this reason it's usually a good idea to pass back all the draws from the composite. ```diff @st.composite def exam_instance_for_exam(draw, exam, student=st.just("")): - return ExamInstance(student=student, exam=exam, answers=draw(answers)) + return exam, student, ExamInstance(student=student, exam=exam, answers=draw(answers)) ``` We also have to adjust `many_exam_instances` and our test. In particular we can't pass `exam_instance_for_exam` directly to the `lists` strategy anymore, as that assumed it only returned an ExamInstance. Now that we're returning a tuple of data it gets a bit messier. ```py @st.composite def many_exam_instances(draw, student=st.just("")): exam = draw(st.from_type(Exam)) student = draw(student) # Unidiomatic/inefficient: getting the values in a loop number = draw(st.integers(1, 5)) instances = [] for _ in range(number): ei_strategy = exam_instance_for_exam(exam=st.just(exam), student=st.just(student)) _, _, ei = draw(ei_strategy) instances.append(ei) # Best practice: use lists(), in this case after .map() instance_strat = exam_instance_for_exam(exam, student).map(lambda x: x[2]) instances = draw(st.lists(instance_strat, min_size=1)) return number, exam, student, instances @given(many_exam_instances(student=st.characters())) def test_8(stuff): _, exam, _, eis = stuff ... ``` As you can see, this leads to a bit more boilerplate. Whether the tradeoffs are worth it depends on your specific case. ## `data` The last way to generate complex inputs is the `data` strategy. Like `composite`, it gives us a `draw` function that we can use to interactively pick values. Unlike `composite`, we can use it in the test body! Instead of `exam_instance_strategy`, we can do this instead: ```py @given(st.data(), st.from_type(Exam)) def test_data(data, exam): answers = data.draw(ei_answers_strategy(exam)) ei = ExamInstance(student=1, exam=exam, answers=answers) assert len(ei.answers) == len(ei.exam.answer_key) ``` The main benefit of `data` is that you can customize your strategy based on the behavior inside the test. Something like: ```py if f(x): y = data.draw(st.integers(min_value=1, max_value=10)) else: y = data.draw(st.integers(min_value=6, max_value=20)) ``` The downside is that `data` doesn't play well with some other Hypothesis features, like the `@example()` decorator, and error reporting is a bit more complicated. If you can generate all your data ahead of time then you're better off using composites. ## Summary Hypothesis can infer a lot using `from_type`. If most random inputs are valid and you just need to rule out rare edge cases, then `assume` and `filter` are simple and effective. Past that, you can use `builds` to generate data with complex invariants. The `composite` strategy gives you even more control and lets you relate multiple draws to each other, making it possible to create lots of interdependent data. Finally, the `data` strategy lets you interactively pick values in the test itself. And all custom strategies can be associated with a type via `register_type_strategy`. Hypothesis provides a lot of mechanisms to create complex data. Learning how to use them well is a bit of an art, but well worth it. Hopefully this makes "what to use when" a little more clear. _Thanks to Zac Hatfield-Dodds and Oskar Wickström for feedback._ ================================================ FILE: website/content/2025-08-07-thread-safe.md ================================================ --- date: 2025-08-07 00:00 title: Hypothesis is now thread-safe author: liam --- *TL;DR: as of [version 6.136.9](https://hypothesis.readthedocs.io/en/latest/changelog.html#v6-136-9), Hypothesis supports running the same test simultaneously from multiple threads.* Hypothesis has historically had the following thread-safety policy: * Running tests in multiple processes: fully supported. * Running separate tests in multiple threads: not officially supported, but mostly worked. * Running the same test in multiple threads: not supported, and didn't work. No longer! In a series of releases spanning from [v6.135.17](https://hypothesis.readthedocs.io/en/latest/changelog.html#v6-135-17) to [v6.136.9](https://hypothesis.readthedocs.io/en/latest/changelog.html#v6-136-9), Hypothesis has gained official support for all three of these cases. You can read about the details of what we now guarantee [here](https://hypothesis.readthedocs.io/en/latest/compatibility.html#thread-safety-policy). The now-historic tracking issue is [here](https://github.com/HypothesisWorks/hypothesis/issues/4451). ## Why now? While we of course would always have loved for Hypothesis to be thread-safe, thread-safety has historically not been a priority, because running Hypothesis tests under multiple threads is not something we see often. That changed recently. Python—as both a language, and a community—is gearing up to [remove the global interpreter lock (GIL)](https://peps.python.org/pep-0703/), in a build called [free threading](https://docs.python.org/3/howto/free-threading-python.html). Python packages, especially those that interact with the C API, will need to test that their code still works under the free threaded build. A great way to do this is to run each test in the suite in two or more threads simultaneously. Where does Hypothesis fit into this? When I was at [PyCon 2025](https://us.pycon.org/2025/) in May earlier this year, I talked with [Nathan Goldbaum](https://github.com/ngoldbaum) from [Quansight](https://quansight.com/), who is one of the people working on community free threading compatibility. Nathan mentioned that because Hypothesis is not thread-safe, Hypothesis tests in community packages have to be skipped when testing free threaded compatibility, which removes a substantial battery of coverage. As a result, Quansight contracted me to work on making Hypothesis thread-safe. I enjoy contributing to Hypothesis in my free time even without a monetary incentive, so this was a pleasure to do, and Nathan and Quansight were great to work with. (Seriously: it's thanks to them funding my time that Hypothesis is now thread-safe!) ## A note on compatibility Free threading may have been the impetus for making Hypothesis thread-safe, but the thread-safety of Hypothesis is not tied to it. Even in the unlikely event that free threading is rolled back tomorrow by the Steering Council, Hypothesis will continue to remain thread-safe. Now, go forth and run Hypothesis in parallel! ================================================ FILE: website/content/2025-11-01-claude-code-plugin.md ================================================ --- date: 2025-11-01 00:00 title: A Claude Code command for Hypothesis authors: liam, maaz, zac-hd, carlini --- *We wrote a paper using Claude to autonomously write and run Hypothesis tests, and found real bugs in numpy, pandas, and other packages. We've extracted this to a Claude Code command for writing Hypothesis tests, which we're sharing today. We hope you find it useful.* *(Not familiar with property-based testing? [Learn more here](https://increment.com/testing/in-praise-of-property-based-testing/)).* --- Hypothesis has shipped with [the ghostwriter](https://hypothesis.readthedocs.io/en/latest/reference/integrations.html#ghostwriter) for quite a while, which automatically writes Hypothesis tests for your code. It uses nothing but good old fashioned heuristics, and is a nice way to stand up Hypothesis tests with minimal effort. Recently, we explored what this same idea might look like with modern AI tools, like Anthropic's Claude Sonnet 4.5 and OpenAI's GPT-5, and the results have been pretty compelling. So we're happy to release `/hypothesis`, a [Claude Code](https://www.claude.com/product/claude-code) command that we developed to automate writing Hypothesis tests. The `/hypothesis` command instructs the model to automatically read your code, infer testable properties, and add Hypothesis tests to your test suite. The idea is that if you wanted to add Hypothesis tests for a file `mypackage/a/utils.py`, you could run `/hypothesis mypackage/a/utils.py`, go get a coffee, and then come back to see some new newly-added tests. You can alternatively give more complex instructions, like `/hypothesis focus on the database implementation; add tests to test_db.py`. We've found `/hypothesis` pretty useful when combined with modern AI models, for tasks ranging from setting up tests in fresh repositories, to augmenting existing test suites, to standing up a full fuzzing workflow with [HypoFuzz](https://hypofuzz.com/). Since `/hypothesis` doesn't (yet) make sense to release in Hypothesis itself, we're releasing it here. [You can find the full command here](https://github.com/HypothesisWorks/hypothesis/blob/master/.claude/commands/hypothesis.md), install it by copying into `~/.claude/commands/`, and run it with `/hypothesis` inside of Claude Code[^1]. # Designing the `/hypothesis` command The broad goal of the `/hypothesis` command is to: (1) look at some code; (2) discover properties that make sense to test; and (3) write Hypothesis tests for those properties. As many developers will attest, often the trickiest part of property-based testing is figuring out what property to test. This is true for modern AI models as well. We therefore design the instructions of `/hypothesis` around gathering as much context about potential properties as it can, before writing any tests. This ensures that the tests the model writes are strongly supported by factual evidence, for example in type hints, docstrings, usage patterns, or existing unit tests. The flow of the `/hypothesis` instructions looks like this: 1. Explore the provided code and identify candidate properties. 2. Explore how the codebases calls that code in practice. 3. Grounded in this understanding, write corresponding Hypothesis tests. 4. Run the new Hypothesis tests, and reflect on any failures. Is it a genuine bug, or is the test incorrect? Refactor the test if necessary. The legwork that `/hypothesis` instructs the model to do both before and after writing a test is critical for deriving high-quality tests. For example, the model might discover in step 2 that a function is called with two different input formats, and both should be tested. Or it might discover in step 4 that it wrote an unsound test, by generating test inputs the function didn't expect, like `math.nan`. ## Failure modes We observed a few failure modes while developing `/hypothesis`. For example, AI models like to write strategies with unnecessary restrictions, like limiting the maximum length of a list even when the property should hold for all lengths of lists. We added explicit instructions in `/hypothesis` not to do this, though that doesn't appear to have fixed the problem entirely. By far the most fundamental failure mode is that the model might simply misunderstand a property in the code. For example, we ran `/hypothesis` on [python-dateutil](https://github.com/dateutil/dateutil); specifically, `/hypothesis src/easter.py`. The model determined that a property of the `easter` function is that it should always return a date on a Sunday, no matter the `method` argument, of which dateutil provides three: `method=EASTER_JULIAN`, `method=EASTER_ORTHODOX`, `method=EASTER_WESTERN`. The model wrote a test saying as much, which then failed, and it proudly claimed it had found a bug. In fact, the model had not found a bug. In reality, `dateutil.easter` computes the date for Easter in the calendar corresponding to the passed `method`, but always returns that date in the Gregorian calendar—which might not be a Sunday. The test written by the model assumed the computation occurred in the Gregorian calendar from start to finish, which was incorrect. This kind of subtle semantic reasoning remains difficult for models, and it's important to keep it in mind as a limitation. # Using `/hypothesis` for bug hunting Armed with a test-writing command, one natural extension is to use it to find real bugs in open-source repositories. To explore this, we used Claude Opus 4.1 to automatically write and run Hypothesis tests for a number of popular Python packages. The results were promising—we found bugs in NumPy, pandas, and Google and Amazon SDKs, and [submitted](https://github.com/numpy/numpy/pull/29609) [patches](https://github.com/aws-powertools/powertools-lambda-python/pull/7246) [for](https://github.com/aws-cloudformation/cloudformation-cli/pull/1106) [several](https://github.com/huggingface/tokenizers/pull/1853) of them. You can [read more in our paper](https://mmaaz-git.github.io/agentic-pbt-site/); it's quite short, so do give it a read if you're interested. It's insightful to walk through one bug we found in particular: a bug in [NumPy's `numpy.random.wald`](https://numpy.org/doc/stable/reference/random/generated/numpy.random.wald.html) function (also called the inverse Gaussian distribution). To start, we ran `/hypothesis numpy.random` to kick off the model. This directs the model to write tests for the entire `numpy.random` module. The model starts by reading the source code of `numpy.random` as well as any relevant docstrings. It sees the function `wald`, realizes from its background knowledge that the mathematical `wald` function should only produce positive values, and tracks that as a potential property. It reads further and discovers from the docstring of `wald` that both the `mean` and `scale` parameters must be greater than 0. Based on this understanding, and a few details from docstrings that we've omitted, the model proposes a range of properties: 1. All outputs of `wald` are positive. 2. No `math.nan` or `math.inf` values are returned on valid inputs. 3. The returned array shape matches the `size` parameter. 4. The `mean` and `scale` arrays broadcasts correctly. 5. Seeding the distribution produces deterministic results. And then goes about writing Hypothesis tests for them. Here's one of the (slightly formatted) tests it writes: ```python import numpy as np from hypothesis import given, strategies as st positive_floats = st.floats( min_value=1e-10, max_value=1e6, allow_nan=False, allow_infinity=False ) @given( mean=positive_floats, scale=positive_floats, size=st.integers(min_value=1, max_value=1000), ) def test_wald_all_outputs_positive(mean, scale, size): """Test that all Wald distribution samples are positive.""" samples = np.random.wald(mean, scale, size) assert np.all(samples > 0), f"Found non-positive values: {samples[samples <= 0]}" ``` It then runs this test. And the test fails! After reflection, the model decides this is a real bug, leaves the test in the test suite, and reports the failure to the developer. What's going on here? We tracked this bug down to catastrophic cancellation in NumPy's `wald` implementation, which could sometimes result in negative values. We reported this to the NumPy maintainers alongside a patch with a more numerically stable algorithm. The NumPy maintainers confirmed the bug, and our fix was released in [v2.3.4](https://github.com/numpy/numpy/releases/tag/v2.3.4). You can [check out the PR here](https://github.com/numpy/numpy/pull/29609). We think this is a really neat confirmation of both the power of property-based testing, and the ability of current AI models to reason about code. # Conclusion We hope you find `/hypothesis` useful in adding Hypothesis tests to your test suites! Developing AI prompts is more of an art than a science; so we encourage you to give any feedback on `/hypothesis` by [opening an issue in the Hypothesis repository](https://github.com/HypothesisWorks/hypothesis/issues/new), even if it's just some open-ended thoughts. [^1]: While Claude Code is currently the most popular tool that supports custom commands, `/hypothesis` is just a markdown file, and works equally as well with any AI framework that supports commands. ================================================ FILE: website/content/2025-11-16-introducing-hypofuzz.md ================================================ --- date: 2025-11-16 00:01 title: Introducing HypoFuzz authors: liam, zac-hd --- If you're reading this blog, you probably know and hopefully love Hypothesis - we've been helping users write better test functions for over a decade now, which you can run with `pytest`, or `unittest`, or any other way you can call a Python function. Getting called just like a traditional unit test is great for interactive development and for CI, but those aren't the only great workflows. What if you could get much more out of your existing tests, with no new work? **That's why we've built [HypoFuzz](https://hypofuzz.com/), which you can use today to find deep bugs and understand your tests**. # Dream workflows Nelson Elhage writes about [two kinds of testing](https://blog.nelhage.com/post/two-kinds-of-testing/): *finding bugs* vs. *catching regressions*. (and also [about PBT and fuzzing](https://blog.nelhage.com/post/property-testing-like-afl/)) To find bugs, you'd want to generate as many random inputs as possible; to catch regressions you'd want a fast and deterministic test suite. Hypothesis can be configured to serve each of these roles, with the `phases=` setting, and database or `@example()` decorator making it easy to move valuable test cases found during the slow and random search into a fast deterministic configuration. This is in fact how CPython uses Hypothesis: in most configurations, their tests *only* run the `@example` cases; and then there are separate CI workers which use the same tests to search for new failing inputs. HypoFuzz lets you push this even further: you can dedicate a server to looking for interesting inputs - whether failing, or just covering some under-tested behavior. We usually give dev machines read-only access to the fuzzing database, so that any failures can be reproduced simply by running the tests locally, but keep CI machines more isolated to avoid blocking work in flight when latent bugs are eventually discovered. # Finding deeper bugs When it comes to finding deep bugs, HypoFuzz has several advantages over Hypothesis - even if you're running them for the same length of time. These are due to either solving a different problem, or using heuristics and techniques which pay off in longer runs but have too much overhead to use in an interactive or CI-style workflow. HypoFuzz uses a more expensive, more powerful approach to input generation, organized around coverage-guided fuzzing. At a high level, we record the code coverage from each input, and can generate new inputs either from scratch, or as variations on a previously generated input. This is particularly useful because once we discover how to trigger rare behavior, we can search that area until the behavior is no longer rare. Even our earliest, very basic implementations of this approach found novel bugs in `hypothesis-jsonschema` which Hypothesis had missed for hundreds of hours. HypoFuzz also dynamically optimizes the allocation of search time *between* test functions, to maximize the overall bug discovery rate. Tests for very simple code can "saturate" relatively quickly; and as they do we'll spend more and more runtime on the remaining tests where more coverage (and bugs) remain to be found. This also allows us to flexibly autoscale the whole system, so you can run HypoFuzz on anything from your laptop, to a dedicated server, to a pool of spot instances or fragments of idle CPU. HypoFuzz dashboard Check out [our literature review](https://hypofuzz.com/docs/literature.html) to read more about these techniques! # Understand your tests Ever written a test and been suspicious when it passes first-try? The dashboard shows input distributions and lets you inspect individual test cases. For particularly interesting tests, you can add custom events for HypoFuzz to graph. HypoFuzz observability [The Tyche vscode extension](https://marketplace.visualstudio.com/items?itemName=HarrisonGoldstein.tyche) gives you similar feedback live in your editor, while the HypoFuzz dashboard can collect, store, and analyze a larger volume of data - including from other machines. We recommend both :-) # Why not open source? We love open source as much as anyone else, and have spent full-time-years worth of work maintaining and improving Hypothesis for free. It's great to hear from middle-schoolers, open-source maintainers, researchers, hobbyists, and professional engineers using Hypothesis, and Hypothesis itself will remain open-source forever. Non-commercial use of HypoFuzz is completely free, too--the more people who can build ambitious things with the confidence fuzzing brings, the happier we'll be. The pitch for *HypoFuzz* though is that we can offer a way to turn money spent on compute into discovered bugs - and we think it's fair for businesses who spend money running HypoFuzz to pay some of that money to us, not just their cloud provider. Finally... if we could pay the bills by working on Hypothesis and HypoFuzz, we'd both have a lot of fun and advance the state of the art a lot faster! The low-hanging fruit is long gone from [our issue tracker](https://github.com/HypothesisWorks/hypothesis/issues), and the remaining issues are often pretty gnarly or require deep enough context that external contributors are rare. # Call to fuzzing! * **[Start using HypoFuzz](https://hypofuzz.com/docs/quickstart.html)**: it's free for non-commercial use, and businesses can [get in touch](mailto:sales@hypofuzz.com?subject=Evaluation%20HypoFuzz%20licence) for a no-cost evaluation license. * **We want to hear from you**, so we can help make HypoFuzz work for you: [get in touch](mailto:hello@hypofuzz.com?subject=HypoFuzz) or [book a chat](https://calendar.app.google/XDBzPvxbLaP2tQLE8) to tell us what you want to see in HypoFuzz, or in general fuzzing / PBT workflows! ================================================ FILE: website/content/pages/testimonials.md ================================================ --- title: Testimonials date: 2015-04-25 22:03 ---

Hypothesis is easy to learn and powerful implementation of property based testing, and generally an invaluable tool. At Lyst we've used it in a wide variety of situations, from testing APIs to machine learning algorithms and in all cases it's given us a great deal more confidence in that code.

Alex Stapleton, Lead Backend Engineer at Lyst

Hypothesis is the single most powerful tool in my toolbox for working with algorithmic code, or any software that produces predictable output from a wide range of sources. When using it with Priority, Hypothesis consistently found errors in my assumptions and extremely subtle bugs that would have taken months of real-world use to locate. In some cases, Hypothesis found subtle deviations from the correct output of the algorithm that may never have been noticed at all.

When it comes to validating the correctness of your tools, nothing comes close to the thoroughness and power of Hypothesis.

Hypothesis has been brilliant for expanding the coverage of our test cases, and also for making them much easier to read and understand, so we're sure we're testing the things we want in the way we want.

At Sixty North we use Hypothesis for testing Segpy, an open source Python library for shifting data between Python data structures and SEG Y files which contain geophysical data from the seismic reflection surveys used in oil and gas exploration.

This is our first experience of property-based testing – as opposed to example-based testing. Not only are our tests more powerful, they are also much better explanations of what we expect of the production code. In fact, the tests are much closer to being specifications. Hypothesis has located real defects in our code which went undetected by traditional test cases, simply because Hypothesis is more relentlessly devious about test case generation than us mere humans! We found Hypothesis particularly beneficial for Segpy because SEG Y is an antiquated format that uses legacy text encodings (EBCDIC) and even a legacy floating point format we implemented from scratch in Python.

Hypothesis is sure to find a place in most of our future Python codebases and many existing ones too.

When I first heard about Hypothesis, I knew I had to include it in my two open-source Python libraries, natsort and fastnumbers.

Quite frankly, I was a little appalled at the number of bugs and "holes" I found in the code. I can now say with confidence that my libraries are more robust to "the wild." In addition, Hypothesis gave me the confidence to expand these libraries to fully support Unicode input, which I never would have had the stomach for without such thorough testing capabilities. Thanks!

Just found out about this excellent QuickCheck for Python implementation and ran up a few tests for my bytesize package last night. Refuted a few hypotheses in the process.

Looking forward to using it with a bunch of other projects as well.

I have written a small library to serialize dicts to MariaDB's dynamic columns binary format, mariadb-dyncol. When I first developed it, I thought I had tested it really well - there were hundreds of test cases, some of them even taken from MariaDB's test suite itself. I was ready to release.

Lucky for me, I tried Hypothesis with David at the PyCon UK sprints. Wow! It found bug after bug after bug. Even after a first release, I thought of a way to make the tests do more validation, which revealed a further round of bugs! Most impressively, Hypothesis found a complicated off-by-one error in a condition with 4095 versus 4096 bytes of data - something that I would never have found.

Long live Hypothesis! (Or at least, property-based testing).

Adopting Hypothesis improved bidict's test coverage and significantly increased our ability to make changes to the code with confidence that correct behavior would be preserved. Thank you, David, for the great testing tool.

One extremely satisfied user here. Hypothesis is a really solid implementation of property-based testing, adapted well to Python, and with good features such as failure-case shrinkers. I first used it on a project where we needed to verify that a vendor's Python and non-Python implementations of an algorithm matched, and it found about a dozen cases that previous example-based testing and code inspections had not. Since then I've been evangelizing for it at our firm.

Your name goes here

Want to add to the list by telling us about your Hypothesis experience? Drop us an email at testimonials@hypothesis.works and we'll add it to the list!

================================================ FILE: website/pelicanconf.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. SITENAME = AUTHOR = "Hypothesis" SITESUBTITLE = "The property-based testing library for Python" # SITEURL = "https://hypothesis.works" PATH = "content" TIMEZONE = "UTC" DELETE_OUTPUT_DIRECTORY = True # Feed generation is usually not desired when developing FEED_ALL_ATOM = None CATEGORY_FEED_ATOM = None TRANSLATION_FEED_ATOM = None AUTHOR_FEED_ATOM = None AUTHOR_FEED_RSS = None ARTICLE_URL = "{category}/{slug}/" ARTICLE_SAVE_AS = "{category}/{slug}/index.html" DEFAULT_CATEGORY = "articles" DISPLAY_PAGES_ON_MENU = False CATEGORY_URL = "articles/" CATEGORY_SAVE_AS = "articles/index.html" # Disable the default archives page ARCHIVES_SAVE_AS = "" FILENAME_METADATA = r"(?P\d{4}-\d{2}-\d{2})-(?P.+)" THEME = "./theme/" STATIC_PATHS = [ "images", "../../brand/favicon.ico", "../../brand/dragonfly-rainbow.svg", "../archive-redirect.html", ] EXTRA_PATH_METADATA = { "../../brand/favicon.ico": {"path": "favicon.ico"}, "../../brand/dragonfly-rainbow.svg": {"path": "dragonfly-rainbow.svg"}, "../archive-redirect.html": {"path": "archives.html"}, } PROFILE_IMAGE_URL = "/dragonfly-rainbow.svg" MENUITEMS = ( ("Blog", "/articles"), ("Docs", "https://hypothesis.readthedocs.io/en/latest/"), ("GitHub", "https://github.com/HypothesisWorks/hypothesis/"), ("PyPI", "https://pypi.org/project/hypothesis/"), ) # Author information - map from short alias to full name and URL AUTHOR_NAMES = { "alexwlchan": "Alex Chan", "carlini": "Nicholas Carlini", "drmaciver": "David R. MacIver", "giorgiosironi": "Giorgio Sironi", "hwayne": "Hillel Wayne", "jml": "Jonathan M. Lange", "liam": "Liam DeVoe", "maaz": "Muhammad Maaz", "nchammas": "Nicholas Chammas", "zac-hd": "Zac Hatfield-Dodds", } AUTHOR_URLS = { "alexwlchan": "https://alexwlchan.net", "carlini": "https://nicholas.carlini.com/", "drmaciver": "http://www.drmaciver.com", "giorgiosironi": "http://giorgiosironi.com", "hwayne": "https://www.hillelwayne.com/", "jml": "https://jml.io", "liam": "https://tybug.dev", "maaz": "https://www.mmaaz.ca/", "nchammas": "http://nchammas.com", "zac-hd": "https://zhd.dev", } assert set(AUTHOR_URLS).issubset(AUTHOR_NAMES) DEFAULT_PAGINATION = False # same as the default from https://docs.getpelican.com/en/latest/settings.html#MARKDOWN, # but with use_pygments = False, since we use prism.js for syntax highlighting # instead. MARKDOWN = { "extension_configs": { "markdown.extensions.codehilite": { "use_pygments": False, "css_class": "highlight", }, "markdown.extensions.extra": {}, "markdown.extensions.meta": {}, }, "output_format": "html5", } # Uncomment following line if you want document-relative URLs when developing # RELATIVE_URLS = True ================================================ FILE: website/theme/static/prism.css ================================================ /* PrismJS 1.17.1 https://prismjs.com/download.html#themes=prism&languages=python * prism.js default theme for JavaScript, CSS and HTML by Lea Verou, based on dabblet.com * Modified by Zac Hatfield-Dodds; removed background, match github's python colors closer, etc. */ pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection { text-shadow: none; } @media print { pre[class*="language-"] { text-shadow: none; } } /* Code blocks */ pre[class*="language-"] { overflow: auto; } /* Ensure tokens don't override line-height */ pre[class*="language-"] * { line-height: inherit; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #6a737d; } .token.punctuation { color: #24292e; } .namespace { opacity: 0.7; } .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol, .token.deleted { color: #005cc5; } .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #032f62; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #24292e; } .token.atrule, .token.attr-value, .token.keyword { color: #d73a49; } .token.function, .token.class-name, .token.decorator { color: #6f42c1; } .token.regex, .token.important, .token.variable { color: #e36209; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } ================================================ FILE: website/theme/static/prism.js ================================================ /* PrismJS 1.30.0 https://prismjs.com/download.html#themes=prism-coy&languages=markup+css+clike+javascript+python */ var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var P=w.value;if(n.length>e.length)return;if(!(P instanceof i)){var E,S=1;if(y){if(!(E=l(b,A,e,m))||E.index>=e.length)break;var L=E.index,O=E.index+E[0].length,C=A;for(C+=w.value.length;L>=C;)C+=(w=w.next).value.length;if(A=C-=w.value.length,w.value instanceof i)continue;for(var j=w;j!==n.tail&&(Cg.reach&&(g.reach=W);var I=w.prev;if(_&&(I=u(n,I,_),A+=_.length),c(n,I,S),w=u(n,I,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),S>1){var T={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,T),g&&T.reach>g.reach&&(g.reach=T.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python; ================================================ FILE: website/theme/static/style.css ================================================ :root { --primary-h: 217; --primary-s: 100%; --primary-l: 19%; --text-h: 0; --text-s: 0%; --color-primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l)); --link-color: hsl(var(--primary-h), 100%, 35%); --font-family: 'Lato', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --text-color: hsl(var(--text-h), var(--text-s), 2%); --nav-link-hover-color: #e9ecef } /* start css reset */ *, *::before, *::after { box-sizing: border-box; } * { margin: 0; } input, button, textarea, select { font: inherit; } /* end css reset */ body { font-family: var(--font-family); background-color: white; } h1, h2, h3 { line-height: 1.2; } code { background-color: #f5f5f5; padding: 0.058rem 0.25rem; border-radius: 4px; color: #c7254e; font-size: 0.92em; } pre { background-color: #f5f5f5; padding: 0.9rem; border-radius: 6px; overflow-x: auto; margin: 1.5rem 0; border: 1px solid #ddd; line-height: 1.1; } pre code { padding: 0; color: var(--text-color); font-size: 0.85rem; } @media only screen and (max-width: 25em) { .profile { max-width: 100%; } } @media print { body { max-width: none; } } .page { font-family: var(--font-family); background-color: white; color: var(--text-color); line-height: 1.6; } .page__container { min-height: 100vh; display: flex; flex-direction: column; } .header { background-color: #f8f9fa; padding: 0.5rem 0; color: var(--text-color); border-bottom: 1px solid #dee2e6; } .header__container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; } .header__brand { flex-shrink: 0; } .brand__link { display: flex; align-items: center; gap: 0.3rem; text-decoration: none; color: inherit; padding: 0rem 0.4rem; border-radius: 4px; transition: color 0.05s ease; } .brand__link:hover { background-color: var(--nav-link-hover-color); border-radius: 4px; } .brand__logo { flex-shrink: 0; } .brand__image { width: 80px; height: 80px; display: block; } .brand__text { display: flex; flex-direction: column; } .brand__title { font-size: 1.5rem; font-weight: 600; line-height: 1.2; color: var(--color-primary); } .brand__subtitle { font-size: 1rem; font-weight: 400; color: hsl(var(--primary-h), 0%, 35%); } .nav__list { display: flex; list-style: none; margin: 0; padding: 0; } .nav__link { color: var(--link-color); text-decoration: none; font-weight: 600; font-size: 1.15rem; padding: 0.75rem 1rem; border-radius: 4px; } .nav__link:hover { color: hsl(var(--primary-h), var(--primary-s), 20%); background-color: var(--nav-link-hover-color); border-radius: 4px; } .main { flex: 1; padding: 3rem 0; } .main__container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; } .home-page { max-width: 850px; margin: 0 auto; } .main-page__header { text-align: center; margin-bottom: 3rem; } .main-page__header-text { font-size: 4.5rem; color: var(--color-primary); line-height: 1.1; margin: 0; } .newsletter-box { background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 2rem; margin-top: 2rem; } .newsletter-box h2 { margin-top: 0; margin-bottom: 1rem; color: var(--color-primary); } .newsletter-box p { margin-bottom: 1.5rem; } .newsletter-form__content { display: flex; gap: 1rem; flex-wrap: wrap; } .newsletter-form__input { padding-left: 1rem; padding-right: 1rem; border: 1px solid #ddd; border-radius: 6px; font-size: 1rem; flex: 1; min-width: 250px; max-width: 350px; } .newsletter-form__input:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 0 2px hsl(var(--primary-h), var(--primary-s), var(--primary-l), 0.1); } .newsletter-form__button { padding: 0.75rem 1.5rem; background-color: var(--color-primary); color: white; border: none; border-radius: 4px; font-size: 1rem; font-weight: 500; cursor: pointer; transition: background-color 0.2s ease; } .newsletter-form__button:hover { background-color: hsl(var(--primary-h), var(--primary-s), 10%); } .articles-page { max-width: 1000px; margin: 0 auto; } .articles-page__header { text-align: center; margin-bottom: 2rem; } .articles-page__title { font-size: 2.5rem; font-weight: 600; color: var(--color-primary); margin-bottom: 0.5rem; } .articles-page__subtitle { font-size: 1.125rem; color: #6c757d; line-height: 1.5; } .articles-grid { display: flex; flex-direction: column; gap: 2rem; } .articles__list { display: flex; flex-direction: column; gap: 2rem; } .article-card { background-color: white; border-radius: 8px; border: 1px solid #ddd; padding-top: 2rem; padding-left: 2rem; padding-right: 2rem; padding-bottom: 1rem; transition: transform 0.05s ease, box-shadow 0.05s ease; cursor: pointer; box-shadow: 0 1.5px 4.5px rgba(0, 0, 0, 0.1); } .article-card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .article-card__header { margin-bottom: 0.75rem; } .article-card__title { font-size: 1.5rem; font-weight: 600; line-height: 1.3; color: var(--color-primary); } .article-card__content ul, .article-card__content ol { margin-bottom: 1rem; padding-left: 2rem; } .article-card__content li { margin-bottom: 0.25rem; } .article-card__content a { color: var(--link-color); text-decoration: none; } .article-card__content p { margin-bottom: 1rem; } .article-card__content a:hover { text-decoration: underline; } .article-card-meta { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; margin-bottom: 0.75rem; } .article-card-meta__separator { font-size: 0.75rem; color: var(--color-primary); } .article-card-meta__item { display: flex; gap: 0.25rem; align-items: center; } .article-card-meta__value { font-size: 1rem; color: var(--color-primary); } .article-card-meta__value a { color: inherit; text-decoration: none; } .article-card-meta__value a:hover { text-decoration: underline; } .article-page { max-width: 850px; margin: 0 auto; } .article__header { margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 2px solid #e2e2e2; } .article__title { font-size: 2.5rem; font-weight: 600; line-height: 1.2; margin-bottom: 1.5rem; color: var(--color-primary); } .article-meta { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; } .article-meta__separator { font-size: 0.75rem; color: var(--color-primary); } .article-meta__item { display: flex; gap: 0.25rem; align-items: center; } .article-meta__value { font-size: 1.2rem; color: var(--color-primary); } .article-meta__value a { color: inherit; text-decoration: none; } .article-meta__value a:hover { text-decoration: underline; } .article-content { line-height: 1.6; font-size: 1.125rem; color: var(--text-color); } .article-content h1, .article-content h2, .article-content h3, .article-content h4, .article-content h5, .article-content h6 { margin-top: 2rem; margin-bottom: 1rem; line-height: 1.3; color: var(--color-primary); } .article-content h1 { font-size: 2rem; } .article-content h2 { font-size: 1.5rem; } .article-content h3 { font-size: 1.25rem; } .article-content p { margin-bottom: 1rem; } .article-content ul, .article-content ol { margin-bottom: 1rem; padding-left: 2rem; } .article-content li { margin-bottom: 0.25rem; } .article-content a { color: var(--link-color); text-decoration: none; } .article-content a:hover { text-decoration: underline; } .article-content hr { margin-top: 1.5rem; margin-bottom: 1.5rem; border: 1px solid #e2e2e2; } .cta-buttons { display: flex; gap: 1.5rem; justify-content: center; align-items: center; margin: 2rem 0; } .cta-button { display: inline-flex; align-items: center; gap: 0.75rem; padding: 0.8rem 1.2rem; background-color: hsl(var(--primary-h), 80%, 56%); color: white !important; text-decoration: none !important; font-weight: 600; font-size: 1.1rem; border: none; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); text-align: center; cursor: pointer; } .cta-icon { width: 1.2rem; height: 1.2rem; filter: brightness(0) invert(1); } .cta-button:hover { background-color: hsl(var(--primary-h), 80%, 51%); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .footnote { padding-top: 1.25rem; } .static-page { max-width: 850px; margin: 0 auto; } .page-content__header { margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px solid #eee; } .page-content__title { font-size: 2.5rem; font-weight: 600; line-height: 1.2; color: var(--color-primary); } .page-body { line-height: 1.7; color: var(--text-color); } .page-body h1, .page-body h2, .page-body h3, .page-body h4, .page-body h5, .page-body h6 { margin-top: 2rem; margin-bottom: 1rem; line-height: 1.3; color: var(--color-primary); } .page-body h1 { font-size: 2rem; } .page-body h2 { font-size: 1.5rem; } .page-body h3 { font-size: 1.25rem; } .page-body p { margin-bottom: 1rem; } .page-body ul, .page-body ol { margin-bottom: 1rem; padding-left: 2rem; } .page-body li { margin-bottom: 0.25rem; } .page-body a { color: var(--link-color); text-decoration: none; } .page-body a:hover { text-decoration: underline; } .footer { background-color: #f8f8f8; border-top: 1px solid #ddd; margin-top: auto; } .footer__container { max-width: 1200px; margin: 0 auto; padding: 1rem; } .footer__content { text-align: center; } .footer__text { color: #666; font-size: 0.875rem; } .separator { border-bottom: 2px solid #cccccc; } @media (max-width: 768px) { .header__container { flex-direction: column; text-align: center; } .nav__list { flex-wrap: wrap; justify-content: center; } .brand__link { flex-direction: column; text-align: center; } .article__title { font-size: 2rem; } .article-card-meta, .article-meta { flex-direction: column; gap: 0.5rem; } .newsletter-form__content { flex-direction: column; align-items: center; } .newsletter-form__input { max-width: 100%; } .articles__title { font-size: 1.75rem; } .cta-buttons { flex-direction: column; gap: 1rem; } } ================================================ FILE: website/theme/templates/article-card.html ================================================
{{ article.title }}
{{ article.date.strftime('%B %d, %Y') }}
{% if article.authors or article.author %}
{% if article.authors %} {% for author in article.authors %} {% set author_name = AUTHOR_NAMES.get(author|string|trim, author|string|trim) %} {% set author_url = AUTHOR_URLS.get(author|string|trim) %} {% if author_url %}{{ author_name }}{% else %}{{ author_name }}{% endif %}{% if not loop.last %}{% if loop.revindex == 2 %}{% if loop.length > 2 %}, and {% else %} and {% endif %}{% else %}, {% endif %}{% endif %} {% endfor %} {% else %} {% set author_name = AUTHOR_NAMES.get(article.author, article.author) %} {% set author_url = AUTHOR_URLS.get(article.author) %} {% if author_url %} {{ author_name }} {% else %} {{ author_name }} {% endif %} {% endif %}
{% endif %}
{{ article.summary }}
================================================ FILE: website/theme/templates/article.html ================================================ {% extends "base.html" %} {% block content %}
{{ article.title }}
{{ article.content }}
{% endblock %} ================================================ FILE: website/theme/templates/base.html ================================================ {% block title %}{{ SITENAME }}{% endblock title %} {% if FEED_ATOM %} {% endif %} {% if FEED_RSS %} {% endif %} {% block head %} {% endblock head %}
{% block content %} {% endblock %}
================================================ FILE: website/theme/templates/category.html ================================================ {% extends "base.html" %} {% block content %}
{{ category.name|capitalize }}
{% for article in articles %} {% include "article-card.html" %} {% endfor %}
{% endblock content %} ================================================ FILE: website/theme/templates/index.html ================================================ {% extends "base.html" %} {% block content %}
Hypothesis

Hypothesis is the property-based testing library for Python. With Hypothesis, you write tests which should pass for all inputs in whatever range you describe, and let Hypothesis randomly choose which of those inputs to check - including edge cases you might not have thought about. For example:

from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_matches_builtin(ls):
  assert sorted(ls) == my_sort(ls)

This randomized testing can catch bugs and edge cases that you didn't think of and wouldn't have found. In addition, when Hypothesis does find a bug, it doesn't just report any failing example — it reports the simplest possible one. This makes property-based tests a powerful tool for debugging, as well as testing.

Recent Articles

{% for article in articles[:3] %} {% include "article-card.html" %} {% endfor %}

More articles from the Hypothesis blog →

{% endblock content %} ================================================ FILE: website/theme/templates/page.html ================================================ {% extends "base.html" %} {% block content %}
{{ page.title }}
{{ page.content }}
{% endblock %} ================================================ FILE: whole_repo_tests/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: whole_repo_tests/documentation/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: whole_repo_tests/documentation/test_documentation.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import importlib import posixpath from pathlib import Path from sphinx.util.inventory import InventoryFile import hypothesis.provisional from hypothesis.strategies import __all__ as STRATEGY_EXPORTS from hypothesistooling.__main__ import documentation from hypothesistooling.projects import hypothesispython as hp def test_documentation(): documentation() def get_all_exported_names(): # start with hypothesis.strategies exports = {f"hypothesis.strategies.{name}" for name in STRATEGY_EXPORTS} # add any strategy exported by hypothesis.provisional exports |= { f"hypothesis.provisional.{name}" for name, value in vars(hypothesis.provisional).items() if getattr(value, "is_hypothesis_strategy_function", False) and not name.startswith("_") } # add anything exported from an extra for p in (hp.PYTHON_SRC / "hypothesis" / "extra").iterdir(): # ignore private files/dirs. Also skip django, which requires setting up # a django app to import: # django.core.exceptions.ImproperlyConfigured: Requested setting # INSTALLED_APPS, but settings are not configured. You must either define # the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() # before accessing settings. if p.name.startswith("_") or p.name == "django": continue module_name = f"hypothesis.extra.{p.stem}" try: module = importlib.import_module(module_name) except ImportError: continue if hasattr(module, "__all__"): exports |= {f"{module_name}.{name}" for name in module.__all__} return exports def test_documents_all_exported_strategies(): # concurrent docs builds to the same output directory does bad things to # sphinx. Build to a test-specific directory, so we don't overlap builds with # any other test that is also building docs. out_dir = "documents_all_exported_strategies" hp.build_docs(to=out_dir) undocumented = get_all_exported_names() - { "hypothesis.extra.numpy.BroadcastableShapes", } # `inventory` looks like: # { # "py:class": { # "hypothesis.HealthCheck": ( # "Hypothesis", # "6.129.4", # "reference/api.html#hypothesis.HealthCheck", # "-", # ), # ... # }, # ... # } inventory_path = ( Path(hp.HYPOTHESIS_PYTHON) / "docs" / "_build" / out_dir / "objects.inv" ) with open(inventory_path, "rb") as f: inventory = InventoryFile.load(f, "", posixpath.join) for items in inventory.values(): undocumented -= set(items.keys()) assert not undocumented, f"undocumented strategies: {sorted(undocumented)}" ================================================ FILE: whole_repo_tests/types/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: whole_repo_tests/types/revealed_types.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re from typing import NamedTuple from hypothesistooling.__main__ import PYTHONS as pythons_map PYTHON_VERSIONS = [v for v in pythons_map if re.fullmatch(r"3\.\d\d?", v)] try: from numpy import __version__ as np_version except ImportError: NP1 = False else: NP1 = np_version.startswith("1.") ASSUME_REVEALED_TYPES = [ ("assume(False)", "Never"), ("assume(None)", "Never"), ("assume(True)", "Literal[True]"), ("assume(1)", "Literal[True]"), ] REVEALED_TYPES = [ ("integers()", "int"), ("text()", "str"), ("integers().map(str)", "str"), ("booleans().filter(bool)", "bool"), ("tuples()", "tuple[()]"), ("tuples(integers())", "tuple[int]"), ("tuples(integers(), text())", "tuple[int, str]"), ( "tuples(integers(), text(), integers(), text(), integers())", "tuple[int, str, int, str, int]", ), ( "tuples(text(), text(), text(), text(), text(), text())", "tuple[Any, ...]", ), ("lists(none())", "list[None]"), ("integers().filter(lambda x: x > 0)", "int"), ("booleans().filter(lambda x: x)", "bool"), ("integers().map(bool).filter(lambda x: x)", "bool"), ("integers().flatmap(lambda x: lists(floats()))", "list[float]"), ("one_of([integers(), integers()])", "int"), ("one_of([integers(), floats()])", "float"), ("one_of([integers(), none()])", "int | None"), ("one_of(integers(), none())", "int | None"), ("one_of(integers(), text())", "int | str"), ("recursive(integers(), lists)", "list[Any] | int"), # We have overloads for up to five types, then fall back to Any. # (why five? JSON atoms are None|bool|int|float|str and we do that a lot) ( "one_of(integers(), text(), none(), binary(), builds(list), builds(dict))", "Any", ), ] class DifferingRevealedTypes(NamedTuple): value: str mypy: str pyright: str DIFF_REVEALED_TYPES = [ DifferingRevealedTypes("none() | integers()", "None | int", "int | None"), DifferingRevealedTypes( "data()", "hypothesis.strategies._internal.core.DataObject", "DataObject" ), # We have overloads for up to five types, then fall back to Any. # (why five? JSON atoms are None|bool|int|float|str and we do that a lot) DifferingRevealedTypes( "one_of(integers(), text(), none(), binary(), builds(list))", "int | str | None | bytes | list[Never]", "int | str | bytes | list[Unknown] | None", ), DifferingRevealedTypes( "dictionaries(integers(), datetimes())", "dict[int, datetime.datetime]", "dict[int, datetime]", ), ] NUMPY_REVEALED_TYPES = [ ( 'arrays(dtype=np.dtype("int32"), shape=1)', "ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]]", ), # ( # "arrays(dtype=np.dtype(int), shape=1)", # "ndarray[tuple[int, ...], dtype[Union[signedinteger[Union[_32Bit, _64Bit]], bool[bool]]]]", # # FIXME: `dtype[signedinteger[_32Bit | _64Bit] | bool[bool]]]]` on mypy now # ), ( "boolean_dtypes()", "dtype[bool[bool]]", # np.bool[builtins.bool] ), ( "unsigned_integer_dtypes(sizes=8)", "dtype[unsignedinteger[_8Bit]]", ), ( "unsigned_integer_dtypes(sizes=16)", "dtype[unsignedinteger[_16Bit]]", ), ( "unsigned_integer_dtypes(sizes=32)", "dtype[unsignedinteger[_32Bit]]", ), ( "unsigned_integer_dtypes(sizes=64)", "dtype[unsignedinteger[_64Bit]]", ), ( "unsigned_integer_dtypes()", "dtype[unsignedinteger[Any]]", ), ( "unsigned_integer_dtypes(sizes=(8, 16))", "dtype[unsignedinteger[Any]]", ), ( "integer_dtypes(sizes=8)", "dtype[signedinteger[_8Bit]]", ), ( "integer_dtypes(sizes=16)", "dtype[signedinteger[_16Bit]]", ), ( "integer_dtypes(sizes=32)", "dtype[signedinteger[_32Bit]]", ), ( "integer_dtypes(sizes=64)", "dtype[signedinteger[_64Bit]]", ), ( "integer_dtypes()", "dtype[signedinteger[Any]]", ), ( "integer_dtypes(sizes=(8, 16))", "dtype[signedinteger[Any]]", ), ( "floating_dtypes(sizes=16)", "dtype[floating[_16Bit]]", ), ( "floating_dtypes(sizes=32)", "dtype[floating[_32Bit]]", ), ( "floating_dtypes(sizes=64)", "dtype[float64]", ), ( "floating_dtypes(sizes=128)", "dtype[floating[_128Bit]]", ), ( "floating_dtypes()", "dtype[floating[Any]]", ), ( "floating_dtypes(sizes=(16, 32))", "dtype[floating[Any]]", ), ( "complex_number_dtypes(sizes=64)", "dtype[complexfloating[_32Bit, _32Bit]]", ), ( "complex_number_dtypes(sizes=128)", "dtype[complex128]", ), ( "complex_number_dtypes(sizes=256)", "dtype[complexfloating[_128Bit, _128Bit]]", ), ( "complex_number_dtypes()", "dtype[complexfloating[Any, Any]]", ), ( "complex_number_dtypes(sizes=(64, 128))", "dtype[complexfloating[Any, Any]]", ), ( "integer_array_indices(shape=(2, 3))", "tuple[ndarray[tuple[Any, ...], dtype[signedinteger[Any]]], ...]", ), ( 'integer_array_indices(shape=(2, 3), dtype=np.dtype("int32"))', "tuple[ndarray[tuple[Any, ...], dtype[signedinteger[_32Bit]]], ...]", ), ( 'integer_array_indices(shape=(2, 3), dtype=np.dtype("uint8"))', "tuple[ndarray[tuple[Any, ...], dtype[unsignedinteger[_8Bit]]], ...]", ), # basic_indices with allow_ellipsis=False (no EllipsisType differences) ( "basic_indices((3, 4), allow_ellipsis=False)", "int | slice[Any, Any, Any] | tuple[int | slice[Any, Any, Any], ...]", ), ] # basic_indices tests where mypy/pyright differ in EllipsisType representation NUMPY_DIFF_REVEALED_TYPES = [ # mypy uses types.EllipsisType, pyright uses EllipsisType DifferingRevealedTypes( "basic_indices((3, 4))", "int | slice[Any, Any, Any] | types.EllipsisType | tuple[int | slice[Any, Any, Any] | types.EllipsisType, ...]", "int | slice[Any, Any, Any] | EllipsisType | tuple[int | slice[Any, Any, Any] | EllipsisType, ...]", ), # pyright also reorders None to the end DifferingRevealedTypes( "basic_indices((3, 4), allow_newaxis=True, allow_ellipsis=False)", "int | slice[Any, Any, Any] | None | tuple[int | slice[Any, Any, Any] | None, ...]", "int | slice[Any, Any, Any] | tuple[int | slice[Any, Any, Any] | None, ...] | None", ), DifferingRevealedTypes( "basic_indices((3, 4), allow_newaxis=True)", "int | slice[Any, Any, Any] | None | types.EllipsisType | tuple[int | slice[Any, Any, Any] | None | types.EllipsisType, ...]", "int | slice[Any, Any, Any] | EllipsisType | tuple[int | slice[Any, Any, Any] | EllipsisType | None, ...] | None", ), ] ================================================ FILE: whole_repo_tests/types/test_hypothesis.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesistooling.projects.hypothesispython import PYTHON_SRC from hypothesistooling.scripts import pip_tool def test_mypy_passes_on_hypothesis(): pip_tool("mypy", str(PYTHON_SRC)) ================================================ FILE: whole_repo_tests/types/test_mypy.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import subprocess import textwrap import pytest from hypothesistooling.projects.hypothesispython import PYTHON_SRC from hypothesistooling.scripts import pip_tool, tool_path from .revealed_types import ( ASSUME_REVEALED_TYPES, DIFF_REVEALED_TYPES, NUMPY_DIFF_REVEALED_TYPES, NUMPY_REVEALED_TYPES, PYTHON_VERSIONS, REVEALED_TYPES, ) @pytest.mark.skip( reason="Hypothesis type-annotates the public API as a convenience for users, " "but strict checks for our internals would be a net drag on productivity." ) def test_mypy_passes_on_hypothesis_strict(): pip_tool("mypy", "--strict", str(PYTHON_SRC)) def get_mypy_output(fname, *extra_args): # Work around a mypy cache bug, # https://github.com/HypothesisWorks/hypothesis/pull/4256 worker_id = os.getenv("PYTEST_XDIST_WORKER", "master") proc = subprocess.run( [ tool_path("mypy"), "--cache-dir", f".mypy_caches/{worker_id}", *extra_args, str(fname), ], encoding="utf-8", capture_output=True, text=True, ) if proc.stderr: raise AssertionError(f"{proc.returncode=}\n\n{proc.stdout}\n\n{proc.stderr}") return proc.stdout def get_mypy_analysed_type(fname): attempts = 0 while True: out = ( get_mypy_output(fname) .rstrip() .removesuffix("Success: no issues found in 1 source file") ) # we've noticed some flakiness in getting an empty output here. Give it # a couple tries. if len(out.splitlines()) == 0: attempts += 1 continue assert len(out.splitlines()) == 1, out assert attempts < 2, "too many failed retries" break # See https://mypy.readthedocs.io/en/latest/common_issues.html#reveal-type # The shell output for `reveal_type([1, 2, 3])` looks like a literal: # file.py:2: error: Revealed type is 'builtins.list[builtins.int*]' return ( out.split("Revealed type is ")[1] .strip() .strip('"' + "'") .replace("builtins.", "") .replace("*", "") .replace( "hypothesis.strategies._internal.strategies.SearchStrategy", "SearchStrategy", ) .replace("numpy._typing.", "") .replace("_nbit_base.", "") .replace("numpy.", "") # .replace("List[", "list[") # .replace("Dict[", "dict[") ) def assert_mypy_errors(fname, expected, python_version=None): _args = ["--no-error-summary", "--show-error-codes"] if python_version: _args.append(f"--python-version={python_version}") out = get_mypy_output(fname, *_args) del _args # Shell output looks like: # file.py:2: error: Incompatible types in assignment ... [assignment] print(f"mypy output: {out}") def convert_lines(): for error_line in out.splitlines(): col, category = error_line.split(":")[-3:-1] if category.strip() != "error": # mypy outputs "note" messages for overload problems, even with # --hide-error-context. Don't include these continue error_code = error_line.split("[")[-1].rstrip("]") if error_code == "empty-body": continue yield (int(col), error_code) assert sorted(convert_lines()) == sorted(expected) @pytest.mark.parametrize( "val,expect", [*REVEALED_TYPES, *((x.value, x.mypy) for x in DIFF_REVEALED_TYPES)], ) def test_revealed_types(tmp_path, val, expect): """Check that Mypy picks up the expected `X` in SearchStrategy[`X`].""" f = tmp_path / "check.py" f.write_text( textwrap.dedent(f""" from hypothesis.strategies import * reveal_type({val}) """), encoding="utf-8", ) typ = get_mypy_analysed_type(f) assert typ == f"SearchStrategy[{expect}]" @pytest.mark.parametrize("val,expect", ASSUME_REVEALED_TYPES) def test_assume_revealed_types(tmp_path, val, expect): """Check that Mypy infers the correct return type for assume().""" f = tmp_path / "check.py" f.write_text( textwrap.dedent(f""" from hypothesis import assume reveal_type({val}) """), encoding="utf-8", ) typ = get_mypy_analysed_type(f) assert typ == expect @pytest.mark.parametrize( "val,expect", [*NUMPY_REVEALED_TYPES, *((x.value, x.mypy) for x in NUMPY_DIFF_REVEALED_TYPES)], ) def test_numpy_revealed_types(tmp_path, val, expect): f = tmp_path / "check.py" f.write_text( textwrap.dedent(f""" import numpy as np from hypothesis.extra.numpy import * reveal_type({val}) """), encoding="utf-8", ) typ = get_mypy_analysed_type(f) assert typ == f"SearchStrategy[{expect}]" @pytest.mark.parametrize( "val,expect", [ ("elements=None, fill=None", "Any"), ("elements=None, fill=floats()", "float"), ("elements=floats(), fill=None", "float"), ("elements=floats(), fill=text()", "object"), # Note: keep this in sync with the equivalent test for Mypy ], ) def test_pandas_column(tmp_path, val, expect): f = tmp_path / "test.py" f.write_text( textwrap.dedent(f""" from hypothesis.extra.pandas import column from hypothesis.strategies import floats, text x = column(name="test", unique=True, dtype=None, {val}) reveal_type(x) """), encoding="utf-8", ) typ = get_mypy_analysed_type(f) assert typ == f"hypothesis.extra.pandas.impl.column[{expect}]" def test_data_object_type_tracing(tmp_path): f = tmp_path / "check_mypy_on_st_data.py" f.write_text( "from hypothesis.strategies import data, integers\n" "d = data().example()\n" "s = d.draw(integers())\n" "reveal_type(s)\n", encoding="utf-8", ) got = get_mypy_analysed_type(f) assert got == "int" def test_drawfn_type_tracing(tmp_path): f = tmp_path / "check_mypy_on_st_drawfn.py" f.write_text( "from hypothesis.strategies import DrawFn, text\n" "def comp(draw: DrawFn) -> str:\n" " s = draw(text(), 123)\n" " reveal_type(s)\n" " return s\n", encoding="utf-8", ) got = get_mypy_analysed_type(f) assert got == "str" def test_composite_type_tracing(tmp_path): f = tmp_path / "check_mypy_on_st_composite.py" f.write_text( "from hypothesis.strategies import composite, DrawFn\n" "@composite\n" "def comp(draw: DrawFn, x: int) -> int:\n" " return x\n" "reveal_type(comp)\n", encoding="utf-8", ) got = get_mypy_analysed_type(f) assert got == "def (x: int) -> SearchStrategy[int]" @pytest.mark.parametrize( "source, expected", [ ("", "def ()"), ("like=f", "def (x: int) -> int"), ("returns=booleans()", "def () -> bool"), ("like=f, returns=booleans()", "def (x: int) -> bool"), ], ) def test_functions_type_tracing(tmp_path, source, expected): f = tmp_path / "check_mypy_on_st_composite.py" f.write_text( "from hypothesis.strategies import booleans, functions\n" "def f(x: int) -> int: return x\n" f"g = functions({source}).example()\n" "reveal_type(g)\n", encoding="utf-8", ) got = get_mypy_analysed_type(f) assert got == expected, (got, expected) def test_settings_preserves_type(tmp_path): f = tmp_path / "check_mypy_on_settings.py" f.write_text( "from hypothesis import settings\n" "@settings(max_examples=10)\n" "def f(x: int) -> int:\n" " return x\n" "reveal_type(f)\n", encoding="utf-8", ) got = get_mypy_analysed_type(f) assert got == "def (x: int) -> int" def test_stateful_bundle_generic_type(tmp_path): f = tmp_path / "check_mypy_on_stateful_bundle.py" f.write_text( "from hypothesis.stateful import Bundle\n" "b: Bundle[int] = Bundle('test')\n" "x = b.example()\n" "reveal_type(x)\n", encoding="utf-8", ) got = get_mypy_analysed_type(f) assert got == "int" @pytest.mark.parametrize("decorator", ["rule", "initialize"]) @pytest.mark.parametrize( "target_args", [ "target=b1", "targets=(b1,)", "targets=(b1, b2)", ], ) @pytest.mark.parametrize("returns", ["int", "MultipleResults[int]"]) def test_stateful_rule_targets(tmp_path, decorator, target_args, returns): f = tmp_path / "check_mypy_on_stateful_rule.py" f.write_text( "from hypothesis.stateful import *\n" "b1: Bundle[int] = Bundle('b1')\n" "b2: Bundle[int] = Bundle('b2')\n" f"@{decorator}({target_args})\n" f"def my_rule() -> {returns}:\n" " ...\n", encoding="utf-8", ) assert_mypy_errors(f, []) @pytest.mark.parametrize("decorator", ["rule", "initialize"]) def test_stateful_rule_no_targets(tmp_path, decorator): f = tmp_path / "check_mypy_on_stateful_rule.py" f.write_text( "from hypothesis.stateful import *\n" f"@{decorator}()\n" "def my_rule() -> None:\n" " ...\n", encoding="utf-8", ) assert_mypy_errors(f, []) @pytest.mark.parametrize("decorator", ["rule", "initialize"]) def test_stateful_target_params_mutually_exclusive(tmp_path, decorator): f = tmp_path / "check_mypy_on_stateful_rule.py" f.write_text( "from hypothesis.stateful import *\n" "b1: Bundle[int] = Bundle('b1')\n" f"@{decorator}(target=b1, targets=(b1,))\n" "def my_rule() -> int:\n" " ...\n", encoding="utf-8", ) # Also outputs "untyped-decorator" error "Untyped decorator makes function "my_rule" # untyped, due to the inability to resolve to an appropriate overloaded # variant assert_mypy_errors(f, [(3, "call-overload"), (3, "untyped-decorator")]) @pytest.mark.parametrize("decorator", ["rule", "initialize"]) @pytest.mark.parametrize( "target_args", ["", "target=b1", "targets=(b1,)", "targets=(b1, b2)"] ) @pytest.mark.parametrize("returns", ["int", "MultipleResults[int]"]) def test_stateful_target_params_return_type(tmp_path, decorator, target_args, returns): f = tmp_path / "check_mypy_on_stateful_rule.py" f.write_text( "from hypothesis.stateful import *\n" "b1: Bundle[str] = Bundle('b1')\n" "b2: Bundle[str] = Bundle('b2')\n" f"@{decorator}({target_args})\n" f"def my_rule() -> {returns}:\n" " ...\n", encoding="utf-8", ) assert_mypy_errors(f, [(4, "arg-type")]) @pytest.mark.parametrize("decorator", ["rule", "initialize"]) def test_stateful_no_target_params_return_type(tmp_path, decorator): f = tmp_path / "check_mypy_on_stateful_rule.py" f.write_text( "from hypothesis.stateful import *\n" f"@{decorator}()\n" "def my_rule() -> int:\n" " ...\n", encoding="utf-8", ) assert_mypy_errors(f, [(2, "arg-type")]) @pytest.mark.parametrize("decorator", ["rule", "initialize"]) @pytest.mark.parametrize("use_multi", [True, False]) def test_stateful_bundle_variance(tmp_path, decorator, use_multi): f = tmp_path / "check_mypy_on_stateful_bundle.py" if use_multi: return_type = "MultipleResults[Dog]" return_expr = "multiple(dog, dog)" else: return_type = "Dog" return_expr = "dog" f.write_text( "from hypothesis.stateful import *\n" "class Animal: pass\n" "class Dog(Animal): pass\n" "a: Bundle[Animal] = Bundle('animal')\n" "d: Bundle[Dog] = Bundle('dog')\n" f"@{decorator}(target=a, dog=d)\n" f"def my_rule(dog: Dog) -> {return_type}:\n" f" return {return_expr}\n", encoding="utf-8", ) assert_mypy_errors(f, []) @pytest.mark.parametrize("decorator", ["rule", "initialize"]) def test_stateful_multiple_return(tmp_path, decorator): f = tmp_path / "check_mypy_on_stateful_multiple.py" f.write_text( "from hypothesis.stateful import *\n" "b: Bundle[int] = Bundle('b')\n" f"@{decorator}(target=b)\n" "def my_rule() -> MultipleResults[int]:\n" " return multiple(1, 2, 3)\n", encoding="utf-8", ) assert_mypy_errors(f, []) @pytest.mark.parametrize("decorator", ["rule", "initialize"]) def test_stateful_multiple_return_invalid(tmp_path, decorator): f = tmp_path / "check_mypy_on_stateful_multiple.py" f.write_text( "from hypothesis.stateful import *\n" "b: Bundle[str] = Bundle('b')\n" f"@{decorator}(target=b)\n" "def my_rule() -> MultipleResults[int]:\n" " return multiple(1, 2, 3)\n", encoding="utf-8", ) assert_mypy_errors(f, [(3, "arg-type")]) @pytest.mark.parametrize( "wrapper,expected", [ ("{}", "int"), ("st.lists({})", "list[int]"), ], ) def test_stateful_consumes_type_tracing(tmp_path, wrapper, expected): f = tmp_path / "check_mypy_on_stateful_rule.py" wrapped = wrapper.format("consumes(b)") f.write_text( "from hypothesis.stateful import *\n" "from hypothesis import strategies as st\n" "b: Bundle[int] = Bundle('b')\n" f"s = {wrapped}\n" "reveal_type(s.example())\n", encoding="utf-8", ) got = get_mypy_analysed_type(f) assert got == expected def test_stateful_consumed_bundle_cannot_be_target(tmp_path): f = tmp_path / "check_mypy_on_stateful_rule.py" f.write_text( "from hypothesis.stateful import *\n" "b: Bundle[int] = Bundle('b')\n" "rule(target=consumes(b))\n", encoding="utf-8", ) assert_mypy_errors(f, [(3, "call-overload")]) @pytest.mark.parametrize( "return_val,errors", [ ("True", []), ("0", [(2, "arg-type"), (2, "return-value")]), ], ) def test_stateful_precondition_requires_predicate(tmp_path, return_val, errors): f = tmp_path / "check_mypy_on_stateful_precondition.py" f.write_text( "from hypothesis.stateful import *\n" f"@precondition(lambda self: {return_val})\n" "def my_rule() -> None: ...\n", encoding="utf-8", ) assert_mypy_errors(f, errors) def test_stateful_precondition_lambda(tmp_path): f = tmp_path / "check_mypy_on_stateful_precondition.py" f.write_text( "from hypothesis.stateful import *\n" "class MyMachine(RuleBasedStateMachine):\n" " valid: bool\n" " @precondition(lambda self: self.valid)\n" " @rule()\n" " def my_rule(self) -> None: ...\n", encoding="utf-8", ) # Note that this doesn't fully check the code because of the `Any` parameter # type. `lambda self: self.invalid` would unfortunately pass too assert_mypy_errors(f, []) def test_stateful_precondition_instance_method(tmp_path): f = tmp_path / "check_mypy_on_stateful_precondition.py" f.write_text( "from hypothesis.stateful import *\n" "class MyMachine(RuleBasedStateMachine):\n" " valid: bool\n" " def check(self) -> bool:\n" " return self.valid\n" " @precondition(check)\n" " @rule()\n" " def my_rule(self) -> None: ...\n", encoding="utf-8", ) assert_mypy_errors(f, []) def test_stateful_precondition_precond_requires_one_arg(tmp_path): f = tmp_path / "check_mypy_on_stateful_precondition.py" f.write_text( "from hypothesis.stateful import *\n" "precondition(lambda: True)\n" "precondition(lambda a, b: True)\n", encoding="utf-8", ) # Additional "Cannot infer type of lambda" errors assert_mypy_errors( f, [(2, "arg-type"), (2, "misc"), (3, "arg-type"), (3, "misc")], ) def test_pos_only_args(tmp_path): f = tmp_path / "check_mypy_on_pos_arg_only_strats.py" f.write_text( textwrap.dedent(""" import hypothesis.strategies as st st.tuples(a1=st.integers()) st.tuples(a1=st.integers(), a2=st.integers()) st.one_of(a1=st.integers()) st.one_of(a1=st.integers(), a2=st.integers()) """), encoding="utf-8", ) assert_mypy_errors( f, [ (4, "call-overload"), (5, "call-overload"), (7, "call-overload"), (8, "call-overload"), ], ) @pytest.mark.parametrize("python_version", PYTHON_VERSIONS) def test_mypy_passes_on_basic_test(tmp_path, python_version): f = tmp_path / "check_mypy_on_basic_tests.py" f.write_text( textwrap.dedent(""" import hypothesis import hypothesis.strategies as st @hypothesis.given(x=st.text()) def test_foo(x: str) -> None: assert x == x from hypothesis import given from hypothesis.strategies import text @given(x=text()) def test_bar(x: str) -> None: assert x == x """), encoding="utf-8", ) assert_mypy_errors(f, [], python_version=python_version) @pytest.mark.parametrize("python_version", PYTHON_VERSIONS) def test_given_only_allows_strategies(tmp_path, python_version): f = tmp_path / "check_mypy_given_expects_strategies.py" f.write_text( textwrap.dedent(""" from hypothesis import given @given(1) def f(): pass """), encoding="utf-8", ) assert_mypy_errors(f, [(4, "call-overload")], python_version=python_version) @pytest.mark.parametrize("python_version", PYTHON_VERSIONS) def test_raises_for_mixed_pos_kwargs_in_given(tmp_path, python_version): f = tmp_path / "raises_for_mixed_pos_kwargs_in_given.py" f.write_text( textwrap.dedent(""" from hypothesis import given from hypothesis.strategies import text @given(text(), x=text()) def test_bar(x): ... """), encoding="utf-8", ) assert_mypy_errors(f, [(5, "call-overload")], python_version=python_version) def test_register_random_interface(tmp_path): f = tmp_path / "test_register_random_interface.py" f.write_text( textwrap.dedent(""" from random import Random from hypothesis import register_random class MyRandom: def __init__(self) -> None: r = Random() self.seed = r.seed self.setstate = r.setstate self.getstate = r.getstate register_random(MyRandom()) register_random(None) # type: ignore[arg-type] """), encoding="utf-8", ) assert_mypy_errors(f, []) def test_register_type_strategy_type_alias_type(tmp_path): # see https://github.com/HypothesisWorks/hypothesis/issues/4410 f = tmp_path / "test_register_type_strategy_type_alias_type.py" f.write_text( textwrap.dedent(""" from hypothesis import strategies as st from typing import TypeAliasType def ints() -> st.SearchStrategy[int]: return st.from_type(int) Ints = TypeAliasType("Ints", int) # or `type Ints = int` # this previous failed type-checking st.register_type_strategy(Ints, ints()) """), encoding="utf-8", ) assert_mypy_errors(f, [], python_version="3.12") ================================================ FILE: whole_repo_tests/types/test_pyright.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations import json import re import subprocess import sys import textwrap from pathlib import Path from typing import Any import pytest from hypothesistooling.projects.hypothesispython import HYPOTHESIS_PYTHON, PYTHON_SRC from hypothesistooling.scripts import pip_tool, tool_path from .revealed_types import ( ASSUME_REVEALED_TYPES, DIFF_REVEALED_TYPES, NUMPY_DIFF_REVEALED_TYPES, NUMPY_REVEALED_TYPES, PYTHON_VERSIONS, REVEALED_TYPES, ) @pytest.mark.skip( reason="Hypothesis type-annotates the public API as a convenience for users, " "but strict checks for our internals would be a net drag on productivity." ) def test_pyright_passes_on_hypothesis(): pip_tool("pyright", "--project", HYPOTHESIS_PYTHON) @pytest.mark.parametrize("python_version", PYTHON_VERSIONS) def test_pyright_passes_on_basic_test(tmp_path: Path, python_version: str): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ import hypothesis import hypothesis.strategies as st @hypothesis.given(x=st.text()) def test_foo(x: str): assert x == x from hypothesis import given from hypothesis.strategies import text @given(x=text()) def test_bar(x: str): assert x == x """ ), encoding="utf-8", ) _write_config( tmp_path, {"typeCheckingMode": "strict", "pythonVersion": python_version} ) assert _get_pyright_errors(file) == [] @pytest.mark.parametrize( "python_version", [ # FIXME: temporary workaround, see hypothesis/pull/4136 pytest.param(v, marks=[pytest.mark.xfail(strict=False)] * (v == "3.13")) for v in PYTHON_VERSIONS ], ) def test_given_only_allows_strategies(tmp_path: Path, python_version: str): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ from hypothesis import given @given(1) def f(): pass """ ), encoding="utf-8", ) _write_config( tmp_path, {"typeCheckingMode": "strict", "pythonVersion": python_version} ) errors = _get_pyright_errors(file) msg = 'Argument of type "Literal[1]" cannot be assigned to parameter "_given_arguments"' assert sum(e["message"].startswith(msg) for e in errors) == 1, errors def test_pyright_issue_3296(tmp_path: Path): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ from hypothesis.strategies import lists, integers lists(integers()).map(sorted) """ ), encoding="utf-8", ) _write_config(tmp_path, {"typeCheckingMode": "strict"}) assert _get_pyright_errors(file) == [] def test_pyright_raises_for_mixed_pos_kwargs_in_given(tmp_path: Path): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ from hypothesis import given from hypothesis.strategies import text @given(text(), x=text()) def test_bar(x: str): pass """ ), encoding="utf-8", ) _write_config(tmp_path, {"typeCheckingMode": "strict"}) assert ( sum( e["message"].startswith( 'No overloads for "given" match the provided arguments' ) for e in _get_pyright_errors(file) ) == 1 ) def test_pyright_issue_3348(tmp_path: Path): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ import hypothesis.strategies as st st.tuples(st.integers(), st.integers()) st.one_of(st.integers(), st.integers()) st.one_of([st.integers(), st.floats()]) # sequence of strats should be OK st.sampled_from([1, 2]) """ ), encoding="utf-8", ) _write_config(tmp_path, {"typeCheckingMode": "strict"}) assert _get_pyright_errors(file) == [] def test_numpy_arrays_strategy(tmp_path: Path): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ import numpy as np from hypothesis.extra.numpy import arrays x = arrays(dtype=np.dtype("int32"), shape=1) """ ), encoding="utf-8", ) _write_config(tmp_path, {"typeCheckingMode": "strict"}) errors = _get_pyright_errors(file) print(errors) assert errors == [] @pytest.mark.parametrize( "val,expect", [*REVEALED_TYPES, *((x.value, x.pyright) for x in DIFF_REVEALED_TYPES)], ) def test_revealed_types(tmp_path, val, expect): """Check that Pyright picks up the expected `X` in SearchStrategy[`X`].""" f = tmp_path / (expect + ".py") f.write_text( textwrap.dedent( f""" from hypothesis.strategies import * reveal_type({val}) """ ), encoding="utf-8", ) _write_config(tmp_path, {"reportWildcardImportFromLibrary ": "none"}) typ = get_pyright_analysed_type(f) assert typ == f"SearchStrategy[{expect}]" @pytest.mark.parametrize("val,expect", ASSUME_REVEALED_TYPES) def test_assume_revealed_types(tmp_path, val, expect): """Check that Pyright infers the correct return type for assume().""" f = tmp_path / "check.py" f.write_text( textwrap.dedent( f""" from hypothesis import assume reveal_type({val}) """ ), encoding="utf-8", ) _write_config(tmp_path) typ = get_pyright_analysed_type(f) # Pyright uses NoReturn, mypy uses Never assert typ == (expect if expect != "Never" else "NoReturn") @pytest.mark.parametrize( "val,expect", [*NUMPY_REVEALED_TYPES, *((x.value, x.pyright) for x in NUMPY_DIFF_REVEALED_TYPES)], ) def test_numpy_revealed_types(tmp_path, val, expect): f = tmp_path / "check.py" f.write_text( textwrap.dedent( f""" import numpy as np from hypothesis.extra.numpy import * reveal_type({val}) """ ), encoding="utf-8", ) _write_config(tmp_path, {"reportWildcardImportFromLibrary ": "none"}) typ = get_pyright_analysed_type(f) assert typ == f"SearchStrategy[{expect}]" @pytest.mark.parametrize( "val,expect", [ ("elements=None, fill=None", "Any"), ("elements=None, fill=floats()", "float"), ("elements=floats(), fill=None", "float"), ("elements=floats(), fill=text()", "float | str"), # Note: keep this in sync with the equivalent test for Mypy ], ) def test_pandas_column(tmp_path, val, expect): f = tmp_path / "test.py" f.write_text( textwrap.dedent( f""" from hypothesis.extra.pandas import column from hypothesis.strategies import * x = column(name="test", unique=True, dtype=None, {val}) reveal_type(x) """ ), encoding="utf-8", ) _write_config( tmp_path, {"typeCheckingMode": "strict", "reportWildcardImportFromLibrary ": "none"}, ) typ = get_pyright_analysed_type(f) assert typ == f"column[{expect}]" def test_pyright_tuples_pos_args_only(tmp_path: Path): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ import hypothesis.strategies as st st.tuples(a1=st.integers()) st.tuples(a1=st.integers(), a2=st.integers()) """ ), encoding="utf-8", ) _write_config(tmp_path, {"typeCheckingMode": "strict"}) assert ( sum( e["message"].startswith( 'No overloads for "tuples" match the provided arguments' ) for e in _get_pyright_errors(file) ) == 2 ) def test_pyright_one_of_pos_args_only(tmp_path: Path): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ import hypothesis.strategies as st st.one_of(a1=st.integers()) st.one_of(a1=st.integers(), a2=st.integers()) """ ), encoding="utf-8", ) _write_config(tmp_path, {"typeCheckingMode": "strict"}) assert ( sum( e["message"].startswith( 'No overloads for "one_of" match the provided arguments' ) for e in _get_pyright_errors(file) ) == 2 ) def test_register_random_protocol(tmp_path: Path): file = tmp_path / "test.py" file.write_text( textwrap.dedent( """ from random import Random from hypothesis import register_random class MyRandom: def __init__(self) -> None: r = Random() self.seed = r.seed self.setstate = r.setstate self.getstate = r.getstate register_random(MyRandom()) register_random(None) # type: ignore """ ), encoding="utf-8", ) _write_config(tmp_path, {"reportUnnecessaryTypeIgnoreComment": True}) assert _get_pyright_errors(file) == [] # ---------- Helpers for running pyright ---------- # def _get_pyright_output(file: Path) -> dict[str, Any]: proc = subprocess.run( [tool_path("pyright"), "--outputjson", f"--pythonpath={sys.executable}"], cwd=file.parent, encoding="utf-8", text=True, capture_output=True, ) try: return json.loads(proc.stdout) except Exception: print(proc.stdout) raise def _get_pyright_errors(file: Path) -> list[dict[str, Any]]: return _get_pyright_output(file)["generalDiagnostics"] def get_pyright_analysed_type(fname): out, *rest = _get_pyright_errors(fname) print(out, rest) assert not rest assert out["severity"] == "information" return ( re.fullmatch(r'Type of ".+" is "(.+)"', out["message"]) .group(1) .replace("builtins.", "") .replace("numpy.", "") .replace( "signedinteger[_32Bit | _64Bit] | bool[bool]", "Union[signedinteger[Union[_32Bit, _64Bit]], bool[bool]]", ) ) def _write_config(config_dir: Path, data: dict[str, Any] | None = None): config = {"extraPaths": [str(PYTHON_SRC)], **(data or {})} (config_dir / "pyrightconfig.json").write_text(json.dumps(config), encoding="utf-8") ================================================ FILE: whole_repo_tests/whole_repo/__init__.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. ================================================ FILE: whole_repo_tests/whole_repo/test_ci_config.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import re from pathlib import Path import pytest from hypothesistooling.__main__ import PYTHONS ci_checks = " ".join( line.strip() for line in Path(".github/workflows/main.yml") .read_text(encoding="utf-8") .splitlines() if "- check-py" in line ) @pytest.mark.parametrize("version", sorted(PYTHONS)) def test_python_versions_are_tested_in_ci(version): slug = version.replace("pypy", "py").replace(".", "") print(ci_checks) assert f"- check-py{slug}" in ci_checks, f"Add {version} to main.yml and tox.ini" def test_python_versions_are_in_trove_classifiers(): got_classifiers = { line.strip(' ",\n') for line in Path("hypothesis-python/pyproject.toml") .read_text(encoding="utf-8") .splitlines() if "Programming Language :: Python :: 3." in line } expected_classifiers = { f"Programming Language :: Python :: 3.{v.split('.')[1]}" for v in PYTHONS.values() if re.fullmatch(r"3\.\d+\.\d+", v) } assert got_classifiers == expected_classifiers ================================================ FILE: whole_repo_tests/whole_repo/test_deploy.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import pytest import hypothesistooling as tools from hypothesistooling import __main__ as main, releasemanagement as rm @pytest.mark.parametrize( "project", [p for p in tools.all_projects() if p.has_release()] ) def test_release_file_exists_and_is_valid(project, monkeypatch): if not tools.has_uncommitted_changes(project.BASE_DIR): pytest.xfail("Cannot run release process with uncommitted changes") monkeypatch.setattr(tools, "create_tag", lambda *args, **kwargs: None) monkeypatch.setattr(tools, "push_tag", lambda name: None) monkeypatch.setattr(rm, "commit_pending_release", lambda p: None) monkeypatch.setattr(project, "upload_distribution", lambda: None) monkeypatch.setattr(project, "IN_TEST", True, raising=False) try: main.do_release(project) with open(project.CHANGELOG_FILE, encoding="utf-8") as i: changelog = i.read() assert project.current_version() in changelog assert rm.release_date_string() in changelog finally: tools.git("checkout", project.BASE_DIR) os.chdir(tools.ROOT) ================================================ FILE: whole_repo_tests/whole_repo/test_release_files.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest import hypothesistooling as tools from hypothesistooling import releasemanagement as rm from hypothesistooling.projects import hypothesispython as hp @pytest.mark.parametrize("project", tools.all_projects()) def test_release_file_exists_and_is_valid(project): if project.has_source_changes(): assert project.has_release(), ( "There are source changes but no RELEASE.rst. Please create " "one to describe your changes. An example can be found in " "RELEASE-sample.rst." ) assert project.has_release_sample(), ( "The RELEASE-sample.rst file is missing. Please copy it " "to RELEASE.rst, rather than moving it." ) rm.parse_release_file(project.RELEASE_FILE) @pytest.mark.skipif(not hp.has_release(), reason="Checking that release") def test_release_file_has_no_merge_conflicts(): _, message = rm.parse_release_file(hp.RELEASE_FILE) assert "<<<" not in message, "Merge conflict in RELEASE.rst" if message in {hp.get_autoupdate_message(x).strip() for x in (True, False)}: return _, *recent_changes, _ = hp.CHANGELOG_ANCHOR.split(hp.changelog(), maxsplit=12) for entry in recent_changes: _, version, old_msg = (x.strip() for x in hp.CHANGELOG_BORDER.split(entry)) assert message not in old_msg, f"Release notes already published for {version}" assert old_msg not in message, f"Copied {version} release notes - merge error?" ================================================ FILE: whole_repo_tests/whole_repo/test_release_management.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import pytest from hypothesistooling.releasemanagement import ( bump_version_info, parse_release_file_contents, release_date_string, replace_assignment_in_string as replace, update_markdown_changelog, ) def parse_release(contents): return parse_release_file_contents(contents, "") def test_update_single_line(): assert replace("a = 1", "a", "2") == "a = 2" def test_update_without_spaces(): assert replace("a=1", "a", "2") == "a=2" def test_update_in_middle(): assert replace("a = 1\nb=2\nc = 3", "b", "4") == "a = 1\nb=4\nc = 3" def test_quotes_string_to_assign(): assert replace("a.c = 1", "a.c", "2") == "a.c = 2" with pytest.raises(ValueError): replace("abc = 1", "a.c", "2") def test_duplicates_are_errors(): with pytest.raises(ValueError): replace("a = 1\na=1", "a", "2") def test_missing_is_error(): with pytest.raises(ValueError): replace("", "a", "1") def test_bump_minor_version(): assert bump_version_info((1, 1, 1), "minor")[0] == "1.2.0" def test_parse_release_file(): assert parse_release("RELEASE_TYPE: patch\nhi") == ("patch", "hi") assert parse_release("RELEASE_TYPE: minor\n\n\n\nhi") == ("minor", "hi") assert parse_release("RELEASE_TYPE: major\n \n\nhi") == ("major", "hi") def test_invalid_release(): with pytest.raises(ValueError): parse_release("RELEASE_TYPE: wrong\nstuff") with pytest.raises(ValueError): parse_release("") TEST_CHANGELOG = f""" # A test project 1.2.3 ({release_date_string()}) some stuff happened # some previous log entry """ def test_update_changelog(tmp_path): path = tmp_path / "CHANGELOG.md" path.write_text("# some previous log entry\n", encoding="utf-8") update_markdown_changelog( str(path), "A test project", "1.2.3", "some stuff happened" ) assert path.read_text(encoding="utf-8").strip() == TEST_CHANGELOG.strip() def test_changelog_parsing_strips_trailing_whitespace(): header = "RELEASE_TYPE: patch\n\n" contents = "Adds a feature\n indented.\n" _level, out = parse_release(header + contents.replace("feature", "feature ")) assert contents.strip() == out ================================================ FILE: whole_repo_tests/whole_repo/test_requirements.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. from hypothesistooling.__main__ import check_requirements def test_requirements(): check_requirements() ================================================ FILE: whole_repo_tests/whole_repo/test_rst_is_valid.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import os import re from pathlib import Path import hypothesistooling as tools from hypothesistooling.projects import hypothesispython as hp from hypothesistooling.scripts import pip_tool def is_sphinx(f): f = os.path.abspath(f) return f.startswith(os.path.join(hp.HYPOTHESIS_PYTHON, "docs")) ALL_RST = [ p for p in tools.all_files() if p.name not in ["RELEASE.rst", "RELEASE-sample.rst"] and p.suffix == ".rst" ] def test_passes_rst_lint(): pip_tool("rst-lint", *(f for f in ALL_RST if not is_sphinx(f))) def test_rst_code_blocks(): # has bitten us before https://github.com/HypothesisWorks/hypothesis/pull/4273 pattern = re.compile(r"^\.\.\s+code-block:\s+", re.MULTILINE) for f in ALL_RST: matches = pattern.search(Path(f).read_text()) assert not matches, ( f"incorrect code block syntax in {f}. Use `.. code-block::` " "instead of `.. code-block:`" ) def disabled_test_passes_flake8(): # TODO: get these whitespace checks without flake8? pip_tool("flake8", "--select=W191,W291,W292,W293,W391", *ALL_RST) ================================================ FILE: whole_repo_tests/whole_repo/test_shellcheck.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import subprocess import hypothesistooling as tools from hypothesistooling import installers as install SCRIPTS = [p for p in tools.all_files() if p.suffix == ".sh"] def test_all_shell_scripts_are_valid(): subprocess.check_call( [install.SHELLCHECK, "--exclude=SC1073,SC1072", *SCRIPTS], cwd=tools.ROOT ) ================================================ FILE: whole_repo_tests/whole_repo/test_validate_branch_check.py ================================================ # This file is part of Hypothesis, which may be found at # https://github.com/HypothesisWorks/hypothesis/ # # Copyright the Hypothesis Authors. # Individual contributors are listed in AUTHORS.rst and the git log. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. import json import os import subprocess import sys from hypothesistooling.projects.hypothesispython import BASE_DIR BRANCH_CHECK = "branch-check" VALIDATE_BRANCH_CHECK = os.path.join(BASE_DIR, "scripts", "validate_branch_check.py") def write_entries(tmp_path, entries): with open(tmp_path / BRANCH_CHECK, "w", encoding="utf-8") as f: f.writelines([json.dumps(entry) + "\n" for entry in entries]) def run_validate_branch_check(tmp_path, *, check, **kwargs): return subprocess.run( [sys.executable, VALIDATE_BRANCH_CHECK], cwd=tmp_path, text=True, capture_output=True, check=check, **kwargs, ) def test_validates_branches(tmp_path): write_entries( tmp_path, [ {"name": name, "value": value} for name in ("first", "second", "third") for value in (False, True) ], ) output = run_validate_branch_check(tmp_path, check=True) assert output.stdout == "Successfully validated 3 branches.\n" def test_validates_one_branch(tmp_path): write_entries( tmp_path, [{"name": "sole", "value": value} for value in (False, True)] ) output = run_validate_branch_check(tmp_path, check=True) assert output.stdout == "Successfully validated 1 branch.\n" def test_fails_on_zero_branches(tmp_path): write_entries(tmp_path, []) output = run_validate_branch_check(tmp_path, check=False) assert output.returncode == 1 assert output.stdout == "No branches found in the branch-check file?\n" def test_reports_uncovered_branches(tmp_path): write_entries( tmp_path, [ {"name": "branch that is always taken", "value": True}, {"name": "some other branch that is never taken", "value": False}, {"name": "covered branch", "value": True}, {"name": "covered branch", "value": False}, ], ) output = run_validate_branch_check(tmp_path, check=False) assert output.returncode == 1 expected = """\ Some branches were not properly covered. The following were always True: * branch that is always taken The following were always False: * some other branch that is never taken """ assert output.stdout == expected