├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── Bug.yaml │ └── Rule_request.md ├── dependabot.yml ├── pull_request_template.md ├── workflows │ ├── depup.yml │ ├── test.yml │ └── wps.yml └── zizmor.yml ├── .gitignore ├── .importlinter ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── .readthedocs.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── action.yml ├── docs ├── Makefile ├── _static │ ├── code-diagram.svg │ ├── diff.png │ ├── legacy.png │ ├── logo.png │ ├── logo.svg │ ├── notebook.png │ ├── notebook_terminal.png │ ├── reviewdog.png │ ├── running.png │ └── terminal.png ├── conf.py ├── index.rst └── pages │ ├── api │ ├── checker.rst │ ├── constants.rst │ ├── contributing.rst │ ├── debugging.rst │ ├── formatter.rst │ ├── glossary.rst │ ├── index.rst │ ├── transformations.rst │ ├── tutorial.rst │ ├── types.rst │ ├── violations.rst │ └── visitors.rst │ ├── changelog │ └── index.rst │ └── usage │ ├── cli.rst │ ├── configuration.rst │ ├── formatter.rst │ ├── integrations │ ├── auto-formatters.rst │ ├── ci.rst │ ├── docker.rst │ ├── editors.rst │ ├── extras.rst │ ├── github-actions.rst │ ├── index.rst │ ├── jupyter_notebooks.rst │ ├── ondivi.rst │ ├── plugins.rst │ └── stubs.rst │ ├── setup.rst │ └── violations │ ├── best_practices.rst │ ├── complexity.rst │ ├── consistency.rst │ ├── index.rst │ ├── naming.rst │ ├── oop.rst │ ├── refactoring.rst │ └── system.rst ├── poetry.lock ├── pyproject.toml ├── scripts ├── action-config.cfg ├── check_generic_visit.py └── entrypoint.sh ├── setup.cfg ├── tests ├── conftest.py ├── fixtures │ ├── formatter │ │ ├── correct.py │ │ ├── formatter1.py │ │ └── formatter2.py │ ├── noqa │ │ ├── noqa.py │ │ └── noqa313.py │ └── notebook.ipynb ├── plugins │ ├── ast_tree.py │ ├── async_sync.py │ ├── compile_code.py │ ├── tokenize_parser.py │ └── violations.py ├── test_checker │ ├── test_exception_handling.py │ ├── test_hypothesis.py │ ├── test_invalid_options.py │ ├── test_module_names.py │ ├── test_noqa.py │ └── test_presets.py ├── test_cli │ ├── __snapshots__ │ │ └── test_explain.ambr │ ├── test_explain.py │ └── test_explain_internals.py ├── test_formatter │ ├── __snapshots__ │ │ └── test_formatter_output.ambr │ └── test_formatter_output.py ├── test_logic │ ├── test_complexity │ │ ├── test_annotations_complexity.py │ │ └── test_cognitive │ │ │ ├── conftest.py │ │ │ └── test_cognitive_complexity.py │ └── test_tree │ │ └── test_functions.py ├── test_options │ ├── test_option_rules.py │ └── test_validate_domain_names_options.py ├── test_regressions │ └── test_regression112.py ├── test_version.py ├── test_violations │ ├── test_codes.py │ ├── test_definition_order.py │ ├── test_docs.py │ └── test_implementation.py ├── test_visitors │ ├── conftest.py │ ├── test_ast │ │ ├── test_blocks │ │ │ └── test_control_variables.py │ │ ├── test_builtins │ │ │ ├── test_assign │ │ │ │ ├── test_multiple_assign.py │ │ │ │ ├── test_unpacking_rules.py │ │ │ │ └── test_unpacking_to_list.py │ │ │ ├── test_collection_hashes │ │ │ │ ├── test_float_keys.py │ │ │ │ └── test_hash_hashable.py │ │ │ ├── test_numbers │ │ │ │ ├── test_approximate_constants.py │ │ │ │ └── test_magic_numbers.py │ │ │ └── test_strings │ │ │ │ ├── test_alphabet_string.py │ │ │ │ └── test_formatted_string.py │ │ ├── test_classes │ │ │ ├── test_base_classes │ │ │ │ ├── test_base_builtin_classes.py │ │ │ │ ├── test_base_exception_base_class.py │ │ │ │ ├── test_class_definition_arguments.py │ │ │ │ └── test_expression_base_class.py │ │ │ ├── test_buggy_super.py │ │ │ ├── test_class_attributes │ │ │ │ ├── test_shadow_attributes.py │ │ │ │ └── test_slots_syntax.py │ │ │ ├── test_getter_setter.py │ │ │ ├── test_lambda_attr_assign.py │ │ │ ├── test_method_order.py │ │ │ ├── test_methods │ │ │ │ ├── test_async_magic_methods.py │ │ │ │ ├── test_async_yield_magic_methods.py │ │ │ │ ├── test_magic_methods.py │ │ │ │ ├── test_method_without_arguments.py │ │ │ │ ├── test_staticmethod.py │ │ │ │ ├── test_useless_overwriting_method.py │ │ │ │ └── test_yield_magic_method.py │ │ │ ├── test_type_var_with_default313.py │ │ │ ├── test_wrong_class_body.py │ │ │ └── test_wrong_empty_lines_count.py │ │ ├── test_compares │ │ │ ├── conftest.py │ │ │ ├── test_conditionals.py │ │ │ ├── test_constant_compares │ │ │ │ └── test_falsy_constant.py │ │ │ ├── test_float_complex_compare.py │ │ │ ├── test_heterogenous_compare.py │ │ │ ├── test_literal.py │ │ │ ├── test_multiple_in.py │ │ │ ├── test_nested_ternary.py │ │ │ └── test_reversed_complex_compare.py │ │ ├── test_complexity │ │ │ ├── test_access │ │ │ │ └── test_access.py │ │ │ ├── test_annotation_complexity │ │ │ │ └── test_annotation_complexity_nesting.py │ │ │ ├── test_call_chains │ │ │ │ └── test_call_chains.py │ │ │ ├── test_classes │ │ │ │ ├── test_bases_classes_counts.py │ │ │ │ ├── test_method_counts.py │ │ │ │ └── test_public_attrs_count.py │ │ │ ├── test_counts │ │ │ │ ├── test_compare_complexity.py │ │ │ │ ├── test_condition_counts.py │ │ │ │ ├── test_decorators_count.py │ │ │ │ ├── test_elifs.py │ │ │ │ ├── test_except_exceptions.py │ │ │ │ ├── test_imports_counts.py │ │ │ │ ├── test_module_counts.py │ │ │ │ ├── test_output_length.py │ │ │ │ ├── test_try_body_length.py │ │ │ │ ├── test_try_except.py │ │ │ │ ├── test_tuple_unpack_length.py │ │ │ │ └── test_type_params312.py │ │ │ ├── test_function │ │ │ │ ├── conftest.py │ │ │ │ ├── test_arguments.py │ │ │ │ ├── test_arguments_lambda.py │ │ │ │ ├── test_asserts_count.py │ │ │ │ ├── test_awaits_count.py │ │ │ │ ├── test_cognitive │ │ │ │ │ ├── test_cognitive_average.py │ │ │ │ │ └── test_cognitive_score.py │ │ │ │ ├── test_expressions.py │ │ │ │ ├── test_local_variables.py │ │ │ │ ├── test_raises.py │ │ │ │ └── test_returns.py │ │ │ ├── test_jones │ │ │ │ ├── test_line_complexity.py │ │ │ │ └── test_module_complexity.py │ │ │ ├── test_nested │ │ │ │ ├── test_nested_classes.py │ │ │ │ └── test_nested_functions.py │ │ │ ├── test_offset_visitor.py │ │ │ ├── test_overuses │ │ │ │ ├── test_overused_expressions.py │ │ │ │ └── test_overused_string.py │ │ │ └── test_pm │ │ │ │ ├── test_match_subjects.py │ │ │ │ └── test_max_match_cases.py │ │ ├── test_conditions │ │ │ ├── test_duplicated_case_patterns.py │ │ │ ├── test_duplicated_if_conditions.py │ │ │ ├── test_negated_conditions.py │ │ │ ├── test_same_element.py │ │ │ └── test_useless_ternary.py │ │ ├── test_decorators │ │ │ └── test_new_style_decorators.py │ │ ├── test_exceptions │ │ │ ├── test_except_block_expression.py │ │ │ ├── test_exception_order.py │ │ │ ├── test_nested_try_blocks.py │ │ │ └── test_try_finally.py │ │ ├── test_functions │ │ │ ├── test_call_context │ │ │ │ ├── test_open_with.py │ │ │ │ ├── test_range_len.py │ │ │ │ └── test_type_compare.py │ │ │ ├── test_complex_default_values.py │ │ │ ├── test_getter_without_return.py │ │ │ ├── test_implicit_primitive.py │ │ │ ├── test_problematic_params.py │ │ │ ├── test_stop_iteration.py │ │ │ ├── test_super_call.py │ │ │ ├── test_unused_variables.py │ │ │ ├── test_useless_lambda.py │ │ │ ├── test_wrong_descriptors.py │ │ │ └── test_wrong_function_calls.py │ │ ├── test_imports │ │ │ ├── test_dotted_raw_import.py │ │ │ ├── test_future_imports.py │ │ │ ├── test_import_object_collision.py │ │ │ ├── test_imports_collision.py │ │ │ ├── test_relative_imports.py │ │ │ └── test_vague_imports.py │ │ ├── test_iterables │ │ │ └── test_unpacking.py │ │ ├── test_keywords │ │ │ ├── test_consistency_returning │ │ │ │ ├── test_consistency_return.py │ │ │ │ └── test_consistency_yield.py │ │ │ ├── test_context_managers │ │ │ │ └── test_context_managers_definitions.py │ │ │ ├── test_del.py │ │ │ ├── test_generator_keywords │ │ │ │ ├── test_consecutive_yields.py │ │ │ │ └── test_yield_from_type.py │ │ │ ├── test_global.py │ │ │ ├── test_keyword_condition.py │ │ │ ├── test_pass.py │ │ │ └── test_raise │ │ │ │ ├── test_raise_from_itself_violation.py │ │ │ │ └── test_system_error_violation.py │ │ ├── test_loops │ │ │ ├── test_comprehensions │ │ │ │ ├── test_for_count.py │ │ │ │ ├── test_ifs_count.py │ │ │ │ └── test_variable_definition_comprehensions.py │ │ │ └── test_loops │ │ │ │ ├── test_await_in_loop.py │ │ │ │ ├── test_for_else.py │ │ │ │ ├── test_infinite_while_loops.py │ │ │ │ ├── test_iter_type.py │ │ │ │ ├── test_lambda_in_loop.py │ │ │ │ ├── test_loop_sum.py │ │ │ │ ├── test_loop_with_else.py │ │ │ │ ├── test_useless_conitue.py │ │ │ │ └── test_variable_definitions_loops.py │ │ ├── test_modules │ │ │ ├── test_empty_init.py │ │ │ ├── test_empty_modules.py │ │ │ ├── test_magic_module_functions.py │ │ │ └── test_mutable_constants.py │ │ ├── test_naming │ │ │ ├── conftest.py │ │ │ ├── test_class_attributes.py │ │ │ ├── test_first_arguments.py │ │ │ ├── test_module_metadata.py │ │ │ ├── test_naming_rules │ │ │ │ ├── test_consecutive_underscore.py │ │ │ │ ├── test_correct.py │ │ │ │ ├── test_long.py │ │ │ │ ├── test_private.py │ │ │ │ ├── test_redability.py │ │ │ │ ├── test_reserved_argument.py │ │ │ │ ├── test_short.py │ │ │ │ ├── test_trailing_underscore.py │ │ │ │ ├── test_underscored_number.py │ │ │ │ ├── test_wrong_names.py │ │ │ │ └── test_wrong_unused_name.py │ │ │ └── test_unused │ │ │ │ ├── test_unused_definition.py │ │ │ │ └── test_variable_usages.py │ │ ├── test_operators │ │ │ ├── test_list_multiply.py │ │ │ ├── test_sign_negation.py │ │ │ ├── test_string_concat.py │ │ │ ├── test_useless_math.py │ │ │ ├── test_useless_operators_before_numbers.py │ │ │ ├── test_walrus.py │ │ │ └── test_zero_div.py │ │ ├── test_pm │ │ │ └── test_extra_subject_syntax.py │ │ ├── test_redundancy │ │ │ └── test_redundant_enumerate.py │ │ ├── test_statements │ │ │ ├── test_almost_swapped.py │ │ │ ├── test_augmented_assign.py │ │ │ ├── test_misrefactored_assignment.py │ │ │ ├── test_not_tuple_argument.py │ │ │ ├── test_pointless_starred.py │ │ │ ├── test_unreachable_code.py │ │ │ ├── test_useless_node.py │ │ │ └── test_wrong_named_keyword.py │ │ └── test_subscripts │ │ │ ├── test_consecutive_slices.py │ │ │ ├── test_float_key_usage.py │ │ │ ├── test_implicit_get.py │ │ │ ├── test_implicit_negative_index.py │ │ │ ├── test_redundant_subscripts.py │ │ │ ├── test_slice_assignment.py │ │ │ └── test_stricter_slice_operations.py │ ├── test_base.py │ ├── test_decorators │ │ └── test_alias_decorator.py │ ├── test_filenames │ │ └── test_module │ │ │ ├── test_module_magic_name.py │ │ │ ├── test_module_name.py │ │ │ ├── test_module_name_length.py │ │ │ ├── test_module_pattern.py │ │ │ ├── test_module_redable_name.py │ │ │ ├── test_module_underscore_number.py │ │ │ └── test_module_underscores.py │ └── test_tokenize │ │ ├── test_comments │ │ ├── conftest.py │ │ ├── test_comment_in_formatted_string312.py │ │ ├── test_empty_comment.py │ │ ├── test_forbidden_noqa.py │ │ ├── test_no_cover_comment.py │ │ ├── test_noqa_comment.py │ │ ├── test_noqa_count.py │ │ ├── test_shebang.py │ │ ├── test_typed_ast.py │ │ └── test_wrong_doc_comment.py │ │ ├── test_conditions │ │ ├── test_implicit_elif.py │ │ └── test_implict_elif_oneline.py │ │ ├── test_consistency │ │ └── test_string_newlines.py │ │ ├── test_primitives │ │ ├── conftest.py │ │ ├── test_numbers │ │ │ ├── test_number_float_zero.py │ │ │ ├── test_number_meaningless_zeros.py │ │ │ └── test_underscored_numbers.py │ │ └── test_string_tokens │ │ │ ├── test_implicit_raw_strings.py │ │ │ ├── test_mulitiline_formatted_string312.py │ │ │ ├── test_string_modifier.py │ │ │ ├── test_string_multiline.py │ │ │ ├── test_unicode_escape.py │ │ │ └── test_unicode_prefix.py │ │ └── test_statements │ │ └── test_multiline_string.py └── whitelist.txt └── wemake_python_styleguide ├── __init__.py ├── checker.py ├── cli ├── __init__.py ├── cli_app.py ├── commands │ ├── __init__.py │ ├── base.py │ └── explain │ │ ├── __init__.py │ │ ├── command.py │ │ ├── message_formatter.py │ │ ├── module_loader.py │ │ └── violation_loader.py └── output.py ├── compat ├── __init__.py ├── aliases.py ├── constants.py ├── functions.py ├── nodes.py ├── packaging.py ├── routing.py └── types.py ├── constants.py ├── formatter.py ├── logic ├── __init__.py ├── arguments │ ├── __init__.py │ ├── call_args.py │ ├── function_args.py │ ├── special_args.py │ └── super_args.py ├── complexity │ ├── __init__.py │ ├── annotations.py │ ├── cognitive.py │ ├── functions.py │ └── overuses.py ├── filenames.py ├── naming │ ├── __init__.py │ ├── access.py │ ├── alphabet.py │ ├── blacklists.py │ ├── builtins.py │ ├── constants.py │ ├── duplicates.py │ ├── enums.py │ ├── logical.py │ └── name_nodes.py ├── nodes.py ├── source.py ├── system.py ├── tokens │ ├── __init__.py │ ├── constants.py │ ├── newlines.py │ ├── numbers.py │ └── strings.py ├── tree │ ├── __init__.py │ ├── annotations.py │ ├── attributes.py │ ├── bools.py │ ├── calls.py │ ├── classes.py │ ├── collections.py │ ├── compares.py │ ├── decorators.py │ ├── exceptions.py │ ├── functions.py │ ├── getters_setters.py │ ├── ifs.py │ ├── imports.py │ ├── keywords.py │ ├── loops.py │ ├── operators.py │ ├── pattern_matching.py │ ├── recursion.py │ ├── slices.py │ ├── strings.py │ ├── stubs.py │ └── variables.py ├── walk.py └── walrus.py ├── options ├── __init__.py ├── config.py ├── defaults.py └── validation.py ├── presets ├── __init__.py ├── topics │ ├── __init__.py │ ├── classes.py │ ├── complexity.py │ └── naming.py └── types │ ├── __init__.py │ ├── file_tokens.py │ ├── filename.py │ └── tree.py ├── py.typed ├── transformations ├── __init__.py ├── ast │ ├── __init__.py │ └── enhancements.py └── ast_tree.py ├── types.py ├── version.py ├── violations ├── __init__.py ├── base.py ├── best_practices.py ├── complexity.py ├── consistency.py ├── naming.py ├── oop.py ├── refactoring.py └── system.py └── visitors ├── __init__.py ├── ast ├── __init__.py ├── blocks.py ├── builtins.py ├── classes │ ├── __init__.py │ ├── attributes.py │ ├── classdef.py │ └── methods.py ├── compares.py ├── complexity │ ├── __init__.py │ ├── access.py │ ├── annotations.py │ ├── calls.py │ ├── classes.py │ ├── counts.py │ ├── function.py │ ├── imports.py │ ├── jones.py │ ├── nested.py │ ├── offset.py │ ├── overuses.py │ └── pm.py ├── conditions.py ├── decorators.py ├── exceptions.py ├── functions.py ├── imports.py ├── iterables.py ├── keywords.py ├── loops.py ├── modules.py ├── naming │ ├── __init__.py │ ├── validation.py │ └── variables.py ├── operators.py ├── pm.py ├── redundancy.py ├── statements.py └── subscripts.py ├── base.py ├── decorators.py ├── filenames ├── __init__.py └── module.py └── tokenize ├── __init__.py ├── comments.py ├── conditions.py ├── functions.py ├── primitives.py ├── statements.py └── syntax.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # Check http://editorconfig.org for more information 2 | # This is the main config file for this project: 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | indent_style = space 10 | insert_final_newline = true 11 | indent_size = 2 12 | 13 | [*.py] 14 | indent_size = 4 15 | 16 | [Makefile] 17 | indent_style = tab 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: wemake-python-styleguide 4 | github: wemake-services 5 | custom: https://boosty.to/sobolevn 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: Create a report to help us improve 3 | labels: 'bug' 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Awesome that you are using our project! 9 | But, sad that it broke :( 10 | 11 | We will try to fix it as soon as possible. 12 | Do you want your features to be implemented faster? 13 | 14 | Please consider supporting our collective: 15 | 👉 https://opencollective.com/wemake-python-styleguide/donate 16 | - type: textarea 17 | attributes: 18 | label: "What's wrong" 19 | description: "Please, explain your problem" 20 | validations: 21 | required: True 22 | - type: textarea 23 | attributes: 24 | label: "How it should be" 25 | description: "Please, explain how do you expect it to be" 26 | validations: 27 | required: True 28 | - type: textarea 29 | attributes: 30 | label: "Flake8 version and plugins" 31 | description: "Contents of `flake8 --bug-report`" 32 | validations: 33 | required: True 34 | - type: textarea 35 | attributes: 36 | label: "pip information" 37 | description: "Contents of `pip --version && pip freeze`" 38 | validations: 39 | required: True 40 | - type: textarea 41 | attributes: 42 | label: "OS information" 43 | description: "Your OS name and version" 44 | validations: 45 | required: True 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Rule_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rule request 3 | about: Request a new rule to be checked 4 | labels: 'rule request' 5 | --- 6 | 7 | # Rule request 8 | 9 | 10 | 11 | ## Thesis 12 | 13 | 14 | 15 | ## Reasoning 16 | 17 | 22 | 23 | 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "02:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "02:00" 14 | open-pull-requests-limit: 10 15 | - package-ecosystem: docker 16 | directory: "/" 17 | schedule: 18 | interval: daily 19 | time: "02:00" 20 | open-pull-requests-limit: 10 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # I have made things! 2 | 3 | 11 | 12 | ## Checklist 13 | 14 | 15 | 16 | - [ ] I have double checked that there are no unrelated changes in this pull request (old patches, accidental config files, etc) 17 | - [ ] I have created at least one test case for the changes I have made 18 | - [ ] I have updated the documentation for the changes I have made 19 | - [ ] I have added my changes to the `CHANGELOG.md` 20 | 21 | ## Related issues 22 | 23 | 33 | 34 | 39 | 40 | 🙏 Please, if you or your company is finding wemake-python-styleguide valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/wemake-python-styleguide. As a thank you, your profile/company logo will be added to our main README which receives hundreds of unique visitors per day. 41 | -------------------------------------------------------------------------------- /.github/workflows/depup.yml: -------------------------------------------------------------------------------- 1 | name: depup 2 | 3 | 'on': 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '30 7 * * *' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | reviewdog: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | persist-credentials: false 22 | - uses: haya14busa/action-depup@v1 23 | id: depup 24 | with: 25 | file: Dockerfile 26 | version_name: REVIEWDOG_VERSION 27 | repo: reviewdog/reviewdog 28 | - name: Create Pull Request to update reviewdog 29 | uses: peter-evans/create-pull-request@v7 30 | with: 31 | token: ${{ secrets.GITHUB_TOKEN }} 32 | title: "chore(deps): update reviewdog to ${{ steps.depup.outputs.latest }}" 33 | commit-message: "chore(deps): update reviewdog to ${{ steps.depup.outputs.latest }}" 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | 'on': 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: ['3.10', '3.11', '3.12', '3.13'] 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | persist-credentials: false 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install poetry 33 | run: | 34 | curl -sSL 'https://install.python-poetry.org' | python 35 | 36 | # Adding `poetry` to `$PATH`: 37 | echo "$HOME/.poetry/bin" >> "$GITHUB_PATH" 38 | - name: Install dependencies 39 | run: | 40 | poetry config virtualenvs.in-project true 41 | poetry run pip install -U pip 42 | poetry install 43 | - name: Run tests 44 | run: make test 45 | - name: Check black 46 | run: | 47 | # We ensure that WPS does not report 48 | # any issues after `black` is applied. 49 | poetry run black . 50 | poetry run flake8 . 51 | - name: Upload coverage to Codecov 52 | uses: codecov/codecov-action@v5 53 | with: 54 | files: ./coverage.xml 55 | token: ${{ secrets.CODECOV_TOKEN }} 56 | -------------------------------------------------------------------------------- /.github/workflows/wps.yml: -------------------------------------------------------------------------------- 1 | name: wps 2 | 3 | 'on': 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | persist-credentials: false 22 | - uses: wemake-services/wemake-python-styleguide@master 23 | with: 24 | reporter: 'github-pr-review' 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.github_token }} 27 | -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | unpinned-uses: 3 | config: 4 | policies: 5 | "*": ref-pin 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-toml 9 | - id: check-xml 10 | - id: check-merge-conflict 11 | - id: check-symlinks 12 | - id: check-illegal-windows-names 13 | - id: mixed-line-ending 14 | args: ["--fix=lf"] 15 | - id: check-case-conflict 16 | - repo: https://github.com/python-jsonschema/check-jsonschema 17 | rev: 0.33.0 18 | hooks: 19 | - id: check-dependabot 20 | - id: check-github-workflows 21 | - id: check-github-actions 22 | - id: check-readthedocs 23 | - repo: https://github.com/rhysd/actionlint 24 | rev: v1.7.7 25 | hooks: 26 | - id: actionlint 27 | additional_dependencies: 28 | - "github.com/wasilibs/go-shellcheck/cmd/shellcheck@latest" 29 | - repo: https://github.com/woodruffw/zizmor-pre-commit 30 | rev: v1.9.0 31 | hooks: 32 | - id: zizmor 33 | - repo: https://github.com/shellcheck-py/shellcheck-py 34 | rev: v0.10.0.1 35 | hooks: 36 | - id: shellcheck 37 | args: ["--severity=style"] 38 | - repo: https://github.com/astral-sh/ruff-pre-commit 39 | rev: v0.11.12 40 | hooks: 41 | - id: ruff 42 | args: ["--exit-non-zero-on-fix"] 43 | - id: ruff-format 44 | 45 | # Should be the last: 46 | - repo: meta 47 | hooks: 48 | - id: check-useless-excludes 49 | 50 | exclude: ^(tests/fixtures/|.*?/__snapshots__/.*) 51 | 52 | ci: 53 | autofix_commit_msg: "[pre-commit.ci] auto fixes from pre-commit.com hooks" 54 | autofix_prs: true 55 | autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate" 56 | autoupdate_schedule: weekly 57 | submodules: false 58 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: wemake-python-styleguide 2 | name: wemake-python-styleguide 3 | description: "Run 'wemake-python-styleguide' for Python linting" 4 | entry: flake8 5 | language: python 6 | types_or: [python, pyi] 7 | args: [] 8 | require_serial: true 9 | additional_dependencies: [] 10 | minimum_pre_commit_version: "2.9.2" 11 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-lts-latest 5 | tools: {python: "3.12"} 6 | jobs: 7 | pre_create_environment: 8 | - asdf plugin add poetry 9 | - asdf install poetry latest 10 | - asdf global poetry latest 11 | - poetry config virtualenvs.create false 12 | - poetry self add poetry-plugin-export 13 | - poetry export --only main --only docs --format=requirements.txt --output=requirements.txt 14 | 15 | python: 16 | install: 17 | - requirements: requirements.txt 18 | - method: pip 19 | path: . 20 | 21 | sphinx: 22 | configuration: 'docs/conf.py' 23 | fail_on_warning: true 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ======================================== 2 | # = Warning! = 3 | # ======================================== 4 | # This is Github Action docker-based image. 5 | # It is not intended for local development! 6 | # 7 | # You can find docs about how to setup your own Github Action here: 8 | # https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/github-actions.html 9 | # 10 | # It can still be used as a raw image for your own containers. 11 | # See `action.yml` in case you want to learn more about GitHub Actions. 12 | # See it live: 13 | # https://github.com/wemake-services/wemake-python-styleguide/actions 14 | # 15 | # This image is also available on Dockerhub: 16 | # https://hub.docker.com/r/wemakeservices/wemake-python-styleguide 17 | 18 | FROM python:3.13.4-alpine 19 | 20 | LABEL maintainer="mail@sobolevn.me" 21 | LABEL vendor="wemake.services" 22 | 23 | ENV WPS_VERSION='1.0.0' 24 | ENV REVIEWDOG_VERSION='v0.20.3' 25 | 26 | RUN apk add --no-cache bash git wget 27 | RUN pip install "wemake-python-styleguide==$WPS_VERSION" \ 28 | # Installing reviewdog to optionally comment on pull requests: 29 | && wget -O - -q 'https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh' \ 30 | | sh -s -- -b /usr/local/bin/ "$REVIEWDOG_VERSION" 31 | 32 | # Custom configuration for this action: 33 | COPY ./scripts/action-config.cfg / 34 | 35 | # Entrypoint: 36 | COPY ./scripts/entrypoint.sh / 37 | RUN chmod +x /entrypoint.sh 38 | ENTRYPOINT ["/entrypoint.sh"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wemake.services 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/usr/bin/env bash 2 | 3 | .PHONY: format 4 | format: 5 | poetry run ruff format 6 | poetry run ruff check 7 | 8 | .PHONY: lint 9 | lint: 10 | poetry run ruff check --exit-non-zero-on-fix --diff 11 | poetry run ruff format --check --diff 12 | poetry run flake8 . 13 | poetry run mypy wemake_python_styleguide scripts 14 | poetry run lint-imports 15 | poetry run python3 scripts/check_generic_visit.py wemake_python_styleguide/visitors/ast 16 | 17 | .PHONY: unit 18 | unit: 19 | poetry run pytest 20 | 21 | .PHONY: package 22 | package: 23 | # TODO: re-enable when poetry@2.0 support will be fixed 24 | # poetry run poetry check 25 | poetry run pip check 26 | 27 | .PHONY: test 28 | test: lint unit package 29 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # This is a definition file for a Github Action. 2 | # See: https://help.github.com/en/articles/creating-a-docker-container-action 3 | 4 | # We also define metadata here: 5 | # See: https://help.github.com/en/articles/metadata-syntax-for-github-actions 6 | 7 | name: 'wemake-python-styleguide' 8 | description: 'Runs wemake-python-styleguide as a GitHub Action' 9 | branding: 10 | icon: 'check' 11 | color: 'green' 12 | inputs: 13 | path: 14 | description: 'Path or space-separated list of paths to lint' 15 | required: false 16 | default: '.' 17 | cwd: 18 | description: 'Path to `cd` into before linting' 19 | required: false 20 | default: '.' 21 | reporter: 22 | description: 'How would you like the results to be displayed?' 23 | required: false 24 | default: 'terminal' 25 | filter_mode: 26 | description: 'How to filter found violations? Only used with `github-pr-*` reporters. Docs: https://github.com/reviewdog/reviewdog#filter-mode' 27 | required: false 28 | default: 'added' 29 | fail_workflow: 30 | description: 'Should we fail the workflow if the check does not pass?' 31 | required: false 32 | default: '1' 33 | outputs: 34 | output: 35 | description: 'The output of wemake-python-styleguide run' 36 | runs: 37 | using: 'docker' 38 | image: 'Dockerfile' 39 | args: 40 | - ${{ inputs.path }} 41 | - ${{ inputs.cwd }} 42 | - ${{ inputs.reporter }} 43 | - ${{ inputs.filter_mode }} 44 | - ${{ inputs.fail_workflow }} 45 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -W 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = wemake-python-styleguide 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/docs/_static/diff.png -------------------------------------------------------------------------------- /docs/_static/legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/docs/_static/legacy.png -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_static/notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/docs/_static/notebook.png -------------------------------------------------------------------------------- /docs/_static/notebook_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/docs/_static/notebook_terminal.png -------------------------------------------------------------------------------- /docs/_static/reviewdog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/docs/_static/reviewdog.png -------------------------------------------------------------------------------- /docs/_static/running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/docs/_static/running.png -------------------------------------------------------------------------------- /docs/_static/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/docs/_static/terminal.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | .. include:: ../README.md 4 | :parser: myst_parser.sphinx_ 5 | 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :caption: User guide: 10 | :hidden: 11 | 12 | pages/usage/setup.rst 13 | pages/usage/configuration.rst 14 | pages/usage/violations/index.rst 15 | pages/usage/formatter.rst 16 | pages/usage/cli.rst 17 | 18 | 19 | .. toctree:: 20 | :maxdepth: 1 21 | :caption: Integrations: 22 | :hidden: 23 | 24 | pages/usage/integrations/index.rst 25 | 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | :caption: Developer's guide: 30 | :hidden: 31 | 32 | pages/api/index.rst 33 | 34 | .. toctree:: 35 | :maxdepth: 1 36 | :caption: Changelog: 37 | :hidden: 38 | 39 | pages/changelog/index.rst 40 | 41 | 42 | Indices and tables 43 | ------------------ 44 | 45 | * :ref:`genindex` 46 | * :ref:`modindex` 47 | * :ref:`search` 48 | -------------------------------------------------------------------------------- /docs/pages/api/checker.rst: -------------------------------------------------------------------------------- 1 | .. _checker: 2 | 3 | Checker 4 | ======= 5 | 6 | .. automodule:: wemake_python_styleguide.checker 7 | :no-members: 8 | -------------------------------------------------------------------------------- /docs/pages/api/constants.rst: -------------------------------------------------------------------------------- 1 | Constants 2 | ========= 3 | 4 | .. automodule:: wemake_python_styleguide.constants 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/pages/api/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | .. include:: ../../../CONTRIBUTING.md 4 | :parser: myst_parser.sphinx_ 5 | -------------------------------------------------------------------------------- /docs/pages/api/debugging.rst: -------------------------------------------------------------------------------- 1 | Debugging 2 | ========= 3 | 4 | In case something does not work the way you want 5 | there are several ways to debug things. 6 | 7 | Viewing module contents 8 | ----------------------- 9 | 10 | We recommend to create a simple file with just the part that does not work. 11 | We usually call this file ``ex.py`` and remove it before the actual commit. 12 | 13 | To reveal internals of this Python source code use: 14 | 15 | * ``python3.12 -m ast ex.py`` 16 | * ``python3.12 -m tokenize ex.py`` 17 | 18 | It might not be enough to find some complex cases, but it helps. 19 | 20 | Test-driven development 21 | ----------------------- 22 | 23 | A lot of people (including @sobolevn) finds 24 | test-driven development really useful to design and debug your code. 25 | 26 | How? 27 | 28 | 1. Write a single test that fails for your new feature or exposes a bug 29 | 2. Run it with ``pytest tests/full/path/to/your/test_module.py`` 30 | 3. Use the magic of ``print`` and ``ast.dump`` to view the contents of nodes 31 | 4. Fix the bug or implement a new feature 32 | 5. Make sure that everything works now: tests must pass 33 | 6. Done! 34 | 35 | Interactive debugging 36 | --------------------- 37 | 38 | We recommend to use ``ipdb`` for interactive debugging 39 | (it is already included as a development package to this project). 40 | 41 | To start interactive debugging session you will need to: 42 | 43 | 1. Set ``export PYTHONBREAKPOINT=ipdb.set_trace`` environment variable 44 | 2. Put ``breakpoint()`` call in places where you need your debugger to stop 45 | 3. Run your program as usual, debugger will stop on places you marked 46 | 47 | This way allows to view local variables, 48 | execute operations step by step, debug complex algorithms. 49 | 50 | Visual debugging 51 | ---------------- 52 | 53 | One can use ``vscode`` or ``pycharm`` to visually debug your app. 54 | In this case you need to setup appropriate entrypoints 55 | and run your app in debug mode. 56 | -------------------------------------------------------------------------------- /docs/pages/api/formatter.rst: -------------------------------------------------------------------------------- 1 | .. _formatter: 2 | 3 | Formatter 4 | --------- 5 | 6 | .. automodule:: wemake_python_styleguide.formatter 7 | :no-members: 8 | -------------------------------------------------------------------------------- /docs/pages/api/transformations.rst: -------------------------------------------------------------------------------- 1 | Transformations 2 | =============== 3 | 4 | Transformations are operations that we perform before the initial work is done. 5 | 6 | There are several types of transformations we do: 7 | 8 | 1. Enhancing the ``ast`` with new properties and features 9 | 10 | .. automodule:: wemake_python_styleguide.transformations.ast_tree 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/pages/api/types.rst: -------------------------------------------------------------------------------- 1 | Types 2 | ===== 3 | 4 | .. automodule:: wemake_python_styleguide.types 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/pages/api/violations.rst: -------------------------------------------------------------------------------- 1 | Violations 2 | ---------- 3 | 4 | .. automodule:: wemake_python_styleguide.violations.base 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/pages/api/visitors.rst: -------------------------------------------------------------------------------- 1 | Visitors 2 | -------- 3 | 4 | .. automodule:: wemake_python_styleguide.visitors.base 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/pages/changelog/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../../CHANGELOG.md 2 | :parser: myst_parser.sphinx_ 3 | -------------------------------------------------------------------------------- /docs/pages/usage/cli.rst: -------------------------------------------------------------------------------- 1 | Command line tool 2 | ================= 3 | 4 | .. versionadded:: 1.1.0 5 | 6 | WPS has a command-line utility named ``wps`` 7 | 8 | Here are listed all the subcommands it has. 9 | 10 | .. rubric:: ``wps explain`` 11 | 12 | This command can be used to get description of violation. 13 | It will be the same description that is located on the website. 14 | 15 | Syntax: ``wps explain `` 16 | 17 | Examples: 18 | 19 | .. code:: text 20 | 21 | $ wps explain WPS115 22 | WPS115 — Require ``snake_case`` for naming class attributes. 23 | 24 | Attributes in Enum and enum-like classes (Django Choices) 25 | are ignored, as they should be written in UPPER_SNAKE_CASE 26 | ... 27 | 28 | .. code:: text 29 | 30 | $ wps explain 116 31 | WPS116 — Forbid using more than one consecutive underscore in variable names. 32 | 33 | Reasoning: 34 | This is done to gain extra readability. 35 | ... 36 | -------------------------------------------------------------------------------- /docs/pages/usage/integrations/auto-formatters.rst: -------------------------------------------------------------------------------- 1 | Auto-formatters 2 | --------------- 3 | 4 | List of supported tools. 5 | 6 | 7 | ruff 8 | ~~~~ 9 | 10 | Fully supported. 11 | You can run ``ruff check && ruff format`` and there 12 | should be no conflicts with ``WPS`` at all. 13 | 14 | But, ``wemake-python-styleguide`` can and will find additional 15 | problems that ``ruff`` missed. 16 | 17 | 18 | isort 19 | ~~~~~ 20 | 21 | We support ``isort``, but we recommend to use ``ruff`` instead. 22 | See https://docs.astral.sh/ruff/rules/#isort-i 23 | 24 | 25 | black 26 | ~~~~~ 27 | 28 | Is supported since ``1.0.0``, but we recommend to use ``ruff format`` instead. 29 | -------------------------------------------------------------------------------- /docs/pages/usage/integrations/ci.rst: -------------------------------------------------------------------------------- 1 | CI 2 | -- 3 | 4 | This guide shows how to use ``flake8`` inside your ``CI``. 5 | 6 | travis 7 | ~~~~~~ 8 | 9 | Here's the minimal configuration required 10 | to set up ``wemake-python-styleguide``, ``flake8``, ``travis`` up and running: 11 | 12 | 1. Learn how to `build python projects with travis `_ 13 | 2. Copy this configuration into your ``.travis.yml``: 14 | 15 | .. code:: yaml 16 | 17 | dist: xenial 18 | language: python 19 | python: 3.7 20 | 21 | install: pip install wemake-python-styleguide 22 | script: flake8 . 23 | 24 | You can also have some inspiration in our own `.travis.yml `_ 25 | configuration file. 26 | 27 | Gitlab CI 28 | ~~~~~~~~~ 29 | 30 | Setting up ``GitlabCI`` is also easy: 31 | 32 | 1. Learn how `Gitlab CI works `_ 33 | 2. Copy this configuration into your ``.gitlab-ci.yml``: 34 | 35 | .. code:: yaml 36 | 37 | image: python3.12 38 | test: 39 | before_script: pip install wemake-python-styleguide 40 | script: flake8 . 41 | 42 | Examples: 43 | 44 | - ``GitlabCI`` + ``python`` `official template `_ 45 | - ``django`` + ``docker`` + ``GitlabCI`` `template `_ 46 | 47 | 48 | pre-commit 49 | ~~~~~~~~~~ 50 | 51 | To setup `pre-commit `_ with ``wemake-python-styleguide``, add a new hook to the project `.pre-commit-config.yaml` file. 52 | 53 | For example: 54 | 55 | .. code:: yaml 56 | 57 | repos: 58 | - repo: https://github.com/wemake-services/wemake-python-styleguide 59 | rev: ... 60 | hooks: 61 | - id: wemake-python-styleguide 62 | -------------------------------------------------------------------------------- /docs/pages/usage/integrations/docker.rst: -------------------------------------------------------------------------------- 1 | Docker 2 | ------ 3 | 4 | .. image:: https://img.shields.io/docker/pulls/wemakeservices/wemake-python-styleguide.svg 5 | :alt: Dockerhub 6 | :target: https://hub.docker.com/r/wemakeservices/wemake-python-styleguide/ 7 | 8 | .. image:: https://images.microbadger.com/badges/image/wemakeservices/caddy-docker.svg 9 | :alt: Image size 10 | :target: https://microbadger.com/images/wemakeservices/wemake-python-styleguide 11 | 12 | We have an existing official image on `DockerHub `_. 13 | 14 | Usage 15 | ~~~~~ 16 | 17 | You can can use it like so: 18 | 19 | .. code:: bash 20 | 21 | docker pull wemakeservices/wemake-python-styleguide 22 | docker run --rm wemakeservices/wemake-python-styleguide . 23 | 24 | Make sure to place proper config file 25 | and mount it with the source code like so: 26 | 27 | .. code:: bash 28 | 29 | docker run --rm wemakeservices/wemake-python-styleguide -v `pwd`:/code /code 30 | 31 | You can also use this image with Gitlab CI or any other container-based CIs. 32 | 33 | Further reading 34 | ~~~~~~~~~~~~~~~ 35 | 36 | - Official `'docker run' docs `_ 37 | - Official `GitlabCI docs `_ 38 | -------------------------------------------------------------------------------- /docs/pages/usage/integrations/editors.rst: -------------------------------------------------------------------------------- 1 | Editors 2 | ------- 3 | 4 | Note, that some editors might need to disable our own :ref:`formatter ` 5 | and set the `default formatter `_ 6 | with ``format = default`` in your configuration. 7 | 8 | - `vscode plugin `_ 9 | - `sublime plugin `_ 10 | - `atom plugin `_ 11 | - `vim plugin `_ 12 | - `emacs plugin `_ 13 | - `pycharm plugin `_ 14 | - `wing plugin `_ 15 | -------------------------------------------------------------------------------- /docs/pages/usage/integrations/extras.rst: -------------------------------------------------------------------------------- 1 | Extras 2 | ------ 3 | 4 | There are some tools that are out of scope of this linter, 5 | however they are super cool. And should definitely be used! 6 | 7 | Things we highly recommend to improve your code quality: 8 | 9 | - `mypy `_ runs type checks on your python code. Finds tons of issues. Makes your code better, improves you as a programmer. You must use, and tell your friends to use it too 10 | - `import-linter `_ allows you to define application layers and ensure you do not break that contract. Absolutely must have 11 | - `cohesion `_ tool to measure code cohesion, works for most of the times. We recommend to use it as a reporting tool 12 | - `dlint `_ tool for encouraging best coding practices and helping ensure Python code is secure 13 | - `vulture `_ allows you to find unused code. Has some drawbacks, since there is too many magic in python code. But, it is still very useful tool for the refactoring 14 | - `bellybutton `_ allows to write linters for custom use-cases. For example, it allows to forbid calling certain (builtins or custom) functions on a per-project bases. No code required, all configuration is written in ``yaml`` 15 | -------------------------------------------------------------------------------- /docs/pages/usage/integrations/index.rst: -------------------------------------------------------------------------------- 1 | Integrations 2 | ------------ 3 | 4 | WPS can integrate with lots of popular and mainstream technologies. In this section you can learn how to use them with WPS. 5 | 6 | .. rubric:: Featured topics 7 | 8 | - :doc:`Using WPS with Ruff ` 9 | - Integrate WPS into editors and IDEs: 10 | 11 | - `vscode plugin `_ 12 | - `vim plugin `_ 13 | - `pycharm plugin `_ 14 | 15 | - :doc:`Run WPS linting in GitHub Actions pipelines ` 16 | 17 | .. toctree:: 18 | :hidden: 19 | 20 | plugins.rst 21 | editors.rst 22 | auto-formatters.rst 23 | ondivi.rst 24 | docker.rst 25 | github-actions.rst 26 | ci.rst 27 | stubs.rst 28 | extras.rst 29 | jupyter_notebooks.rst 30 | -------------------------------------------------------------------------------- /docs/pages/usage/integrations/jupyter_notebooks.rst: -------------------------------------------------------------------------------- 1 | .. _jupyter_notebooks: 2 | 3 | Jupyter Notebooks 4 | ----------------- 5 | 6 | ``flake8`` does not run on Jupyter Notebooks out-of-the-box. However, there exist projects 7 | such as `nbqa `_ and 8 | `flake8-nb `_ which allow you to do so. 9 | 10 | Due to some error/warning codes not applying naturally to Jupyter Notebooks 11 | (e.g. "missing module docstring"), it may be a good idea to ignore some of them, 12 | for example by running: 13 | 14 | .. code:: bash 15 | 16 | $ nbqa flake8 notebook.ipynb --extend-ignore=NIP102,D100,E302,E305,E703,WPS102,WPS114 17 | 18 | For example, if we have a file ``notebook.ipynb`` 19 | 20 | .. image:: https://raw.githubusercontent.com/MarcoGorelli/wemake-python-styleguide/issue-1704/docs/_static/notebook.png 21 | 22 | we can run this project on this as follows: 23 | 24 | .. image:: https://raw.githubusercontent.com/MarcoGorelli/wemake-python-styleguide/issue-1704/docs/_static/notebook_terminal.png 25 | -------------------------------------------------------------------------------- /docs/pages/usage/integrations/stubs.rst: -------------------------------------------------------------------------------- 1 | Stubs 2 | ----- 3 | 4 | If you are using stub ``.pyi`` files 5 | and `flake8-pyi `_ extension 6 | you might need to ignore several violations that are bundled with this linter. 7 | 8 | You can still do it on per-file bases as usual. 9 | Use ``*.pyi`` glob to list ignored violations: 10 | 11 | .. code:: ini 12 | 13 | # Inside `setup.cfg`: 14 | [flake8] 15 | per-file-ignores = 16 | *.pyi: WPS604 17 | 18 | You can look at the `returns `_ 19 | project as an example. 20 | -------------------------------------------------------------------------------- /docs/pages/usage/violations/best_practices.rst: -------------------------------------------------------------------------------- 1 | .. _best-practices: 2 | 3 | Best practices 4 | ============== 5 | 6 | .. automodule:: wemake_python_styleguide.violations.best_practices 7 | :no-members: 8 | -------------------------------------------------------------------------------- /docs/pages/usage/violations/complexity.rst: -------------------------------------------------------------------------------- 1 | .. _complexity: 2 | 3 | Complexity 4 | ========== 5 | 6 | .. automodule:: wemake_python_styleguide.violations.complexity 7 | :no-members: 8 | -------------------------------------------------------------------------------- /docs/pages/usage/violations/consistency.rst: -------------------------------------------------------------------------------- 1 | .. _consistency: 2 | 3 | Consistency 4 | =========== 5 | 6 | .. automodule:: wemake_python_styleguide.violations.consistency 7 | :no-members: 8 | -------------------------------------------------------------------------------- /docs/pages/usage/violations/index.rst: -------------------------------------------------------------------------------- 1 | Violations 2 | ---------- 3 | 4 | Here we have all violation codes listed for this plugin and its dependencies. 5 | Our violation codes are using ``WPS`` letters. 6 | Other codes are coming from other tools. 7 | 8 | 9 | .. rubric:: Our own codes 10 | 11 | ============== ====== 12 | Type Codes 13 | -------------- ------ 14 | System :ref:`WPS000 - WPS099 ` 15 | Naming :ref:`WPS100 - WPS199 ` 16 | Complexity :ref:`WPS200 - WPS299 ` 17 | Consistency :ref:`WPS300 - WPS399 ` 18 | Best practices :ref:`WPS400 - WPS499 ` 19 | Refactoring :ref:`WPS500 - WPS599 ` 20 | OOP :ref:`WPS600 - WPS699 ` 21 | ============== ====== 22 | 23 | .. toctree:: 24 | :maxdepth: 0 25 | :caption: Violation types: 26 | :hidden: 27 | 28 | system.rst 29 | naming.rst 30 | complexity.rst 31 | consistency.rst 32 | best_practices.rst 33 | refactoring.rst 34 | oop.rst 35 | -------------------------------------------------------------------------------- /docs/pages/usage/violations/naming.rst: -------------------------------------------------------------------------------- 1 | .. _naming: 2 | 3 | Naming 4 | ====== 5 | 6 | .. automodule:: wemake_python_styleguide.violations.naming 7 | :no-members: 8 | -------------------------------------------------------------------------------- /docs/pages/usage/violations/oop.rst: -------------------------------------------------------------------------------- 1 | .. _oop: 2 | 3 | OOP 4 | === 5 | 6 | .. automodule:: wemake_python_styleguide.violations.oop 7 | :no-members: 8 | -------------------------------------------------------------------------------- /docs/pages/usage/violations/refactoring.rst: -------------------------------------------------------------------------------- 1 | .. _refactoring: 2 | 3 | Refactoring 4 | =========== 5 | 6 | .. automodule:: wemake_python_styleguide.violations.refactoring 7 | :no-members: 8 | -------------------------------------------------------------------------------- /docs/pages/usage/violations/system.rst: -------------------------------------------------------------------------------- 1 | .. _system: 2 | 3 | System 4 | ====== 5 | 6 | .. automodule:: wemake_python_styleguide.violations.system 7 | :no-members: 8 | -------------------------------------------------------------------------------- /scripts/action-config.cfg: -------------------------------------------------------------------------------- 1 | # We need to overload some default configuration, 2 | # when running inside GitHub Actions + ReviewDog. 3 | # Because we struggle to get the correct formatting. 4 | 5 | [flake8] 6 | # Base flake8 configuration: 7 | # https://flake8.pycqa.org/en/latest/user/configuration.html 8 | format = default 9 | show-source = false 10 | statistics = false 11 | -------------------------------------------------------------------------------- /scripts/check_generic_visit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | from typing import Final, NoReturn 4 | 5 | import astpath 6 | from pygments import highlight 7 | from pygments.formatters.terminal256 import Terminal256Formatter 8 | from pygments.lexers.python import PythonLexer 9 | 10 | FAIL_CODE: Final = 255 11 | 12 | OK_CODE: Final = 0 13 | 14 | PATTERN: Final = """ 15 | //ClassDef[contains(bases, Name[@id='BaseNodeVisitor'])]/body 16 | /FunctionDef[re:match('visit_.*', @name) 17 | and not(child::body/Expr[last()]/value/Call/func/Attribute[@attr='generic_visit'] or 18 | child::body/With[last()]/body/Expr[last()]/value/Call/func/Attribute[@attr='generic_visit'])] 19 | """ # noqa: E501 20 | 21 | # This is needed to stop linter from spewing WPS421 errors. 22 | report = print 23 | 24 | 25 | def main() -> NoReturn: 26 | """Check for ``self.generic_visit()`` in all visit methods.""" 27 | if len(sys.argv) == 1: 28 | report('Please provide path to search in!') 29 | 30 | matches = astpath.search(sys.argv[1], PATTERN, print_matches=False) 31 | 32 | if not len(matches): 33 | sys.exit(OK_CODE) 34 | 35 | report() 36 | report('"self.generic_visit(node)" should be last statement here:') 37 | 38 | for fn, line in matches: 39 | lines = Path(fn).read_text(encoding='utf8').splitlines() 40 | report( 41 | '\t{}:{}\n\t{}'.format( 42 | fn, 43 | line, 44 | highlight( 45 | lines[line - 1], 46 | PythonLexer(), 47 | Terminal256Formatter(), 48 | ), 49 | ), 50 | ) 51 | 52 | sys.exit(FAIL_CODE) 53 | 54 | 55 | if __name__ == '__main__': 56 | main() 57 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # All configuration for plugins 2 | # and other utils is defined here and in `pyproject.toml` 3 | 4 | # === Linter configuration === 5 | # You can reuse this configuration in your own projects. 6 | 7 | 8 | # NOTE: You can use https://pypi.org/project/Flake8-pyproject/ 9 | # to move all your `flake8` configuration to `pyproject.toml` 10 | 11 | [flake8] 12 | # Base flake8 configuration: 13 | # https://flake8.pycqa.org/en/latest/user/configuration.html 14 | format = wemake 15 | show-source = true 16 | statistics = false 17 | doctests = true 18 | 19 | # Self settings: 20 | max-imports = 17 21 | 22 | # Excluding some directories: 23 | extend-exclude = 24 | .venv 25 | # These folders contain code badly written for reasons: 26 | # Project specific, do not copy. 27 | tests/fixtures/** 28 | tests/**/__snapshots__/** 29 | 30 | # We only run `wemake-python-styleguide` with `flake8`: 31 | select = WPS, E999 32 | 33 | per-file-ignores = 34 | # These modules should contain a lot of classes and strings: 35 | wemake_python_styleguide/violations/*.py: WPS202, WPS226 36 | # This module should contain magic numbers: 37 | wemake_python_styleguide/options/defaults.py: WPS432 38 | # Checker has a lot of imports: 39 | wemake_python_styleguide/checker.py: WPS201 40 | # Allows mypy type hinting, `Ellipsis`` usage, multiple methods: 41 | wemake_python_styleguide/types.py: WPS214, WPS220 42 | # There are multiple fixtures, `assert`s, and subprocesses in tests: 43 | tests/test_visitors/test_ast/test_naming/conftest.py: WPS202 44 | tests/*.py: WPS202, WPS211, WPS226 45 | # Docs can have the configuration they need: 46 | docs/conf.py: WPS407 47 | # Pytest fixtures 48 | tests/plugins/*.py: WPS442 49 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from collections import namedtuple 3 | 4 | import pytest 5 | 6 | from wemake_python_styleguide.options.config import Configuration 7 | 8 | pytest_plugins = [ 9 | 'plugins.violations', 10 | 'plugins.compile_code', 11 | 'plugins.ast_tree', 12 | 'plugins.tokenize_parser', 13 | 'plugins.async_sync', 14 | ] 15 | 16 | 17 | @pytest.fixture(scope='session') 18 | def absolute_path(): 19 | """Fixture to create full path relative to `contest.py` inside tests.""" 20 | 21 | def factory(*files: str) -> pathlib.Path: 22 | dirname = pathlib.Path(__file__).parent 23 | return dirname.joinpath(*files) 24 | 25 | return factory 26 | 27 | 28 | @pytest.fixture(scope='session') 29 | def options(): 30 | """Returns the options builder.""" 31 | default_values = { 32 | option.long_option_name[2:].replace('-', '_'): option.default 33 | for option in Configuration._options # noqa: SLF001 34 | } 35 | 36 | Options = namedtuple('options', default_values.keys()) 37 | 38 | def factory(**kwargs): 39 | final_options = default_values.copy() 40 | final_options.update(kwargs) 41 | return Options(**final_options) 42 | 43 | return factory 44 | 45 | 46 | @pytest.fixture(scope='session') 47 | def default_options(options): 48 | """Returns the default options.""" 49 | return options() 50 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/correct.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module level docstring. 3 | 4 | They are required. 5 | """ 6 | 7 | 8 | def clear_name(good_name: int) -> int: 9 | """All functions should be like this one.""" 10 | return good_name + good_name 11 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/formatter1.py: -------------------------------------------------------------------------------- 1 | def s(handle: int) -> int: 2 | return handle + 2_00 3 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/formatter2.py: -------------------------------------------------------------------------------- 1 | def data(param) -> int: 2 | return s + 10_00 3 | -------------------------------------------------------------------------------- /tests/fixtures/noqa/noqa313.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all possible violations for python 3.13+. 3 | 4 | It is used for e2e tests. 5 | """ 6 | 7 | class NewStyleGenerics[ 8 | TypeVarDefault=int, 9 | *FollowingTuple=*tuple[int, ...] # noqa: WPS477 10 | ]: 11 | """TypeVarTuple follows a defaulted TypeVar.""" 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Correct cell" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "def clear_name(good_name: int) -> int:\n", 17 | " \"\"\"All functions should be like this one.\"\"\"\n", 18 | " return good_name + good_name" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "Incorrect cells" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "def s(handle: int) -> int:\n", 35 | " return handle + 2_00" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "def data(param) -> int:\n", 45 | " return s + 10_00" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [] 54 | } 55 | ], 56 | "metadata": { 57 | "anaconda-cloud": {}, 58 | "kernelspec": { 59 | "display_name": "Python 3", 60 | "language": "python", 61 | "name": "python3" 62 | }, 63 | "language_info": { 64 | "codemirror_mode": { 65 | "name": "ipython", 66 | "version": 3 67 | }, 68 | "file_extension": ".py", 69 | "mimetype": "text/x-python", 70 | "name": "python", 71 | "nbconvert_exporter": "python", 72 | "pygments_lexer": "ipython3", 73 | "version": "3.8.0-final" 74 | } 75 | }, 76 | "nbformat": 4, 77 | "nbformat_minor": 4 78 | } 79 | -------------------------------------------------------------------------------- /tests/plugins/ast_tree.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from textwrap import dedent 3 | 4 | import pytest 5 | 6 | from wemake_python_styleguide.transformations.ast_tree import transform 7 | 8 | 9 | @pytest.fixture(scope='session') 10 | def parse_ast_tree(compile_code): 11 | """ 12 | Function to convert code to AST. 13 | 14 | This helper mimics some transformations that generally 15 | happen in different ``flake8`` plugins that we rely on. 16 | 17 | This list can be extended only when there's a direct need to 18 | replicate the existing behavior from other plugin. 19 | 20 | It is better to import and reuse the required transformation. 21 | But in case it is impossible to do, you can reinvent it. 22 | 23 | Order is important. 24 | """ 25 | 26 | def factory(code: str, *, do_compile: bool = True) -> ast.AST: 27 | code_to_parse = dedent(code) 28 | if do_compile: 29 | compile_code(code_to_parse) 30 | return transform(ast.parse(code_to_parse)) 31 | 32 | return factory 33 | -------------------------------------------------------------------------------- /tests/plugins/async_sync.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def async_wrapper(): 6 | """Fixture to convert all regular functions into async ones.""" 7 | 8 | def factory(template: str) -> str: 9 | return ( 10 | template.replace( 11 | 'def ', 12 | 'async def ', 13 | ) 14 | .replace( 15 | 'with ', 16 | 'async with ', 17 | ) 18 | .replace( 19 | 'for ', 20 | 'async for ', 21 | ) 22 | ) 23 | 24 | return factory 25 | 26 | 27 | @pytest.fixture 28 | def regular_wrapper(): 29 | """Fixture to return regular functions without modifications.""" 30 | 31 | def factory(template: str) -> str: 32 | return template 33 | 34 | return factory 35 | 36 | 37 | @pytest.fixture(params=['async_wrapper', 'regular_wrapper']) 38 | def mode(request): 39 | """Fixture that returns either `async` or regular functions.""" 40 | return request.getfixturevalue(request.param) 41 | -------------------------------------------------------------------------------- /tests/plugins/compile_code.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='session') 5 | def compile_code(): 6 | """ 7 | Compiles given string to Python's AST. 8 | 9 | We need to compile to check some syntax features 10 | that are validated after the ``ast`` is processed: 11 | like double arguments or ``break`` outside of loops. 12 | """ 13 | 14 | def factory(code_to_parse: str) -> None: 15 | compile(code_to_parse, '', 'exec') # noqa: WPS421 16 | 17 | return factory 18 | -------------------------------------------------------------------------------- /tests/plugins/tokenize_parser.py: -------------------------------------------------------------------------------- 1 | import io 2 | import tokenize 3 | from pathlib import Path 4 | from textwrap import dedent 5 | 6 | import pytest 7 | 8 | 9 | @pytest.fixture(scope='session') 10 | def parse_tokens(compile_code): 11 | """Parses tokens from a string.""" 12 | 13 | def factory( 14 | code: str, 15 | *, 16 | do_compile: bool = True, 17 | ) -> list[tokenize.TokenInfo]: 18 | code = dedent(code) 19 | if do_compile: 20 | compile_code(code) 21 | lines = io.StringIO(code) 22 | return list(tokenize.generate_tokens(lambda: next(lines))) 23 | 24 | return factory 25 | 26 | 27 | @pytest.fixture(scope='session') 28 | def parse_file_tokens(parse_tokens, compile_code): 29 | """Parses tokens from a file.""" 30 | 31 | def factory( 32 | filename: str, 33 | ) -> list[tokenize.TokenInfo]: 34 | file_content = Path(filename).read_text(encoding='utf-8') 35 | compile_code(file_content) 36 | return parse_tokens(file_content) 37 | 38 | return factory 39 | -------------------------------------------------------------------------------- /tests/test_checker/test_exception_handling.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from contextlib import suppress 3 | 4 | from wemake_python_styleguide.checker import Checker 5 | from wemake_python_styleguide.violations.system import InternalErrorViolation 6 | from wemake_python_styleguide.visitors.base import BaseNodeVisitor 7 | 8 | 9 | class _BrokenVisitor(BaseNodeVisitor): 10 | def visit(self, _tree) -> None: 11 | raise ValueError('Message from visitor') 12 | 13 | 14 | def test_exception_handling( 15 | default_options, 16 | capsys, 17 | ): 18 | """Ensures that checker works with module names.""" 19 | Checker.parse_options(default_options) 20 | checker = Checker(tree=ast.parse(''), file_tokens=[], filename='test.py') 21 | checker._visitors = [_BrokenVisitor] # noqa: SLF001 22 | 23 | with suppress(StopIteration): 24 | violation = next(checker.run()) 25 | assert violation[2][7:] == InternalErrorViolation.error_template 26 | 27 | captured = capsys.readouterr() 28 | assert 'ValueError: Message from visitor' in captured.out 29 | -------------------------------------------------------------------------------- /tests/test_checker/test_invalid_options.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | 4 | def test_invalid_options(absolute_path): 5 | """End-to-End test to check option validation works.""" 6 | process = subprocess.Popen( 7 | [ 8 | 'flake8', 9 | '--isolated', 10 | '--select', 11 | 'WPS', 12 | '--max-imports', 13 | '-5', # should be positive 14 | absolute_path('fixtures', 'noqa.py'), 15 | ], 16 | stdout=subprocess.PIPE, 17 | stderr=subprocess.PIPE, 18 | universal_newlines=True, 19 | encoding='utf8', 20 | ) 21 | _, stderr = process.communicate() 22 | 23 | assert process.returncode == 1 24 | assert 'ValueError' in stderr 25 | 26 | 27 | def test_invalid_domain_names_options(absolute_path): 28 | """End-to-End test to check domain names options validation works.""" 29 | process = subprocess.Popen( 30 | [ 31 | 'flake8', 32 | '--isolated', 33 | '--select', 34 | 'WPS', 35 | # values from `allowed-domain-names` cannot intersect with 36 | # `--forbidden-domain-names` 37 | '--allowed-domain-names', 38 | 'item,items,handle,visitor', 39 | '--forbidden-domain-names', 40 | 'handle,visitor,node', 41 | absolute_path('fixtures', 'noqa.py'), 42 | ], 43 | stdout=subprocess.PIPE, 44 | stderr=subprocess.PIPE, 45 | universal_newlines=True, 46 | encoding='utf8', 47 | ) 48 | _, stderr = process.communicate() 49 | 50 | assert process.returncode == 1 51 | assert 'ValueError' in stderr 52 | assert 'handle' in stderr 53 | assert 'visitor' in stderr 54 | -------------------------------------------------------------------------------- /tests/test_checker/test_module_names.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import pytest 4 | 5 | from wemake_python_styleguide.checker import Checker 6 | from wemake_python_styleguide.violations import naming 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('filename', 'error'), 11 | [ 12 | ('__magic__.py', naming.WrongModuleMagicNameViolation), 13 | ('util.py', naming.WrongModuleNameViolation), 14 | ('x.py', naming.TooShortNameViolation), 15 | ('test__name.py', naming.ConsecutiveUnderscoresInNameViolation), 16 | ('123py.py', naming.WrongModuleNamePatternViolation), 17 | ('version_1.py', naming.UnderscoredNumberNameViolation), 18 | ('__private.py', naming.PrivateNameViolation), 19 | ( 20 | 'oh_no_not_an_extremely_super_duper_unreasonably_long_name.py', 21 | naming.TooLongNameViolation, 22 | ), 23 | ('привет', naming.UnicodeNameViolation), 24 | ], 25 | ) 26 | def test_module_names(filename, error, default_options): 27 | """Ensures that checker works with module names.""" 28 | Checker.parse_options(default_options) 29 | checker = Checker(tree=ast.parse(''), file_tokens=[], filename=filename) 30 | _line, _col, error_text, _type = next(checker.run()) 31 | 32 | assert int(error_text[3:6]) == error.code 33 | -------------------------------------------------------------------------------- /tests/test_cli/__snapshots__/test_explain.ambr: -------------------------------------------------------------------------------- 1 | # serializer version: 1 2 | # name: test_command 3 | ''' 4 | WPS123 — Forbid unused variables with multiple underscores. 5 | 6 | Reasoning: 7 | We only use ``_`` as a special definition for an unused variable. 8 | Other variables are hard to read. It is unclear why would one use it. 9 | 10 | Solution: 11 | Rename unused variables to ``_`` 12 | or give it some more context with an explicit name: ``_context``. 13 | 14 | Example:: 15 | 16 | # Correct: 17 | some_element, _next_element, _ = some_tuple() 18 | some_element, _, _ = some_tuple() 19 | some_element, _ = some_tuple() 20 | 21 | # Wrong: 22 | some_element, _, __ = some_tuple() 23 | 24 | .. versionadded:: 0.12.0 25 | 26 | See at website: https://pyflak.es/WPS123 27 | 28 | ''' 29 | # --- 30 | # name: test_command_on_not_found[wps explain 10000] 31 | ''' 32 | Violation "10000" not found 33 | 34 | ''' 35 | # --- 36 | # name: test_command_on_not_found[wps explain NOT_A_CODE] 37 | ''' 38 | Violation "NOT_A_CODE" not found 39 | 40 | ''' 41 | # --- 42 | # name: test_command_on_not_found[wps explain WPS10000] 43 | ''' 44 | Violation "WPS10000" not found 45 | 46 | ''' 47 | # --- 48 | # name: test_no_command_specified 49 | ''' 50 | usage: wps [-h] {explain} ... 51 | wps: error: the following arguments are required: {explain} 52 | 53 | ''' 54 | # --- 55 | -------------------------------------------------------------------------------- /tests/test_cli/test_explain.py: -------------------------------------------------------------------------------- 1 | """Integration testing of wps explain command.""" 2 | 3 | import subprocess 4 | 5 | import pytest 6 | 7 | 8 | def _popen_in_shell(args: str) -> tuple[subprocess.Popen, str, str]: 9 | """Run command in shell.""" 10 | # shell=True is needed for subprocess.Popen to 11 | # locate the installed wps command. 12 | process = subprocess.Popen( # noqa: S602 (insecure shell=True) 13 | args, 14 | stdout=subprocess.PIPE, 15 | stderr=subprocess.PIPE, 16 | text=True, 17 | shell=True, 18 | ) 19 | stdin, stdout = process.communicate() 20 | return process, stdin, stdout 21 | 22 | 23 | def test_command(snapshot): 24 | """Test that command works and formats violations as expected.""" 25 | process, stdout, stderr = _popen_in_shell('wps explain WPS123') 26 | assert process.returncode == 0, (stdout, stderr) 27 | assert stdout == snapshot 28 | assert not stderr 29 | 30 | 31 | @pytest.mark.parametrize( 32 | 'command', 33 | [ 34 | 'wps explain 10000', 35 | 'wps explain NOT_A_CODE', 36 | 'wps explain WPS10000', 37 | ], 38 | ) 39 | def test_command_on_not_found(command, snapshot): 40 | """Test command works when violation code is wrong.""" 41 | process, stdout, stderr = _popen_in_shell(command) 42 | assert process.returncode == 1, (stdout, stderr) 43 | assert not stdout 44 | assert stderr == snapshot 45 | 46 | 47 | def test_no_command_specified(snapshot): 48 | """Test command displays error message when no subcommand provided.""" 49 | process, stdout, stderr = _popen_in_shell('wps') 50 | stdout, stderr = process.communicate() 51 | assert process.returncode != 0, (stdout, stderr) 52 | assert not stdout 53 | assert stderr == snapshot 54 | -------------------------------------------------------------------------------- /tests/test_cli/test_explain_internals.py: -------------------------------------------------------------------------------- 1 | """Unit testing of wps explain command.""" 2 | 3 | import pytest 4 | 5 | from wemake_python_styleguide.cli.commands.explain import ( 6 | violation_loader, 7 | ) 8 | from wemake_python_styleguide.violations.best_practices import ( 9 | InitModuleHasLogicViolation, 10 | ) 11 | from wemake_python_styleguide.violations.naming import ( 12 | UpperCaseAttributeViolation, 13 | ) 14 | from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation 15 | 16 | 17 | @pytest.mark.parametrize( 18 | 'violation_params', 19 | [ 20 | (115, UpperCaseAttributeViolation), 21 | (412, InitModuleHasLogicViolation), 22 | (600, BuiltinSubclassViolation), 23 | ], 24 | ) 25 | def test_violation_getter(violation_params): 26 | """Test that violation loader can get violation by their codes.""" 27 | violation_code, expected_class = violation_params 28 | violation = violation_loader.get_violation(violation_code) 29 | assert violation.code is not None 30 | assert violation.docstring == expected_class.__doc__ 31 | -------------------------------------------------------------------------------- /tests/test_logic/test_complexity/test_annotations_complexity.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.logic.complexity import annotations 4 | 5 | 6 | @pytest.mark.parametrize( 7 | ('annotation', 'complexity'), 8 | [ 9 | # simple annotations 10 | ('str', 1), 11 | ('int', 1), 12 | ('List', 1), 13 | ('List[str]', 2), 14 | ('List[int]', 2), 15 | ('Dict[str, int]', 2), 16 | # empty values 17 | ('Literal[""]', 2), 18 | ('Tuple[()]', 2), 19 | # Literals with strings: 20 | ('Literal["regular", "raise", "is"]', 2), 21 | # invalid annotations 22 | ('"This is rainbow in the dark!"', 1), 23 | # complex annotations 24 | ('Tuple[List[int], Optional[Dict[str, int]]]', 4), 25 | ], 26 | ) 27 | def test_get_annotation_complexity( 28 | parse_ast_tree, 29 | annotation: str, 30 | complexity: int, 31 | ) -> None: 32 | """Test get_annotation_complexity function.""" 33 | text = f'def f() -> {annotation}: pass\n' 34 | tree = parse_ast_tree(text) 35 | node = tree.body[0].returns 36 | assert annotations.get_annotation_complexity(node) == complexity 37 | -------------------------------------------------------------------------------- /tests/test_logic/test_complexity/test_cognitive/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fixtures to make testing cognitive complexity easy. 3 | 4 | Policy for testing cognitive complexity: 5 | 6 | 1. Use a single function def in code samples 7 | 2. Write ``# +x`` comments on each line where addition happens 8 | 9 | Adapted from https://github.com/Melevir/cognitive_complexity 10 | """ 11 | 12 | import ast 13 | 14 | import pytest 15 | 16 | from wemake_python_styleguide.compat.aliases import FunctionNodes 17 | from wemake_python_styleguide.logic.complexity import cognitive 18 | 19 | 20 | def _find_function(tree: ast.AST) -> FunctionNodes: # pragma: no cover 21 | for node in ast.walk(tree): 22 | if isinstance(node, FunctionNodes): 23 | return node 24 | raise ValueError('No function definition found', tree) 25 | 26 | 27 | @pytest.fixture(scope='session') 28 | def get_code_snippet_complexity(parse_ast_tree): 29 | """Fixture to parse and count cognitive complexity the easy way.""" 30 | 31 | def factory(src: str) -> int: 32 | funcdef = _find_function(parse_ast_tree(src)) 33 | return cognitive.cognitive_score(funcdef) 34 | 35 | return factory 36 | -------------------------------------------------------------------------------- /tests/test_options/test_option_rules.py: -------------------------------------------------------------------------------- 1 | from wemake_python_styleguide.options import config 2 | 3 | 4 | def test_option_docs(): 5 | """Ensures that all options are documented.""" 6 | for option in config.Configuration._options: # noqa: SLF001 7 | option_value = option.long_option_name[2:] 8 | option_name = f'``{option_value}``' 9 | assert option_name in config.__doc__ 10 | 11 | 12 | def test_option_help(): 13 | """Ensures that all options has help.""" 14 | for option in config.Configuration._options: # noqa: SLF001 15 | assert len(option.help) > 10 16 | assert '%(default)s' in option.help 17 | assert option.help.split(' Defaults to:')[0].endswith('.') 18 | 19 | 20 | def test_option_asdict_no_none(): 21 | """Ensure that `None` is not returned from `asdict_no_none()`.""" 22 | opt = config._Option( # noqa: SLF001 23 | '--foo', 24 | default=False, 25 | action='store_true', 26 | type=None, 27 | help='', 28 | ) 29 | assert 'type' not in opt.asdict_no_none() 30 | -------------------------------------------------------------------------------- /tests/test_options/test_validate_domain_names_options.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.options.validation import ( 4 | validate_domain_names_options, 5 | ) 6 | 7 | 8 | @pytest.mark.parametrize( 9 | ('allowed_names', 'forbidden_names'), 10 | [ 11 | (['items'], []), 12 | ([], ['items']), 13 | (['item'], ['handle']), 14 | ], 15 | ) 16 | def test_passes_when_any_option_not_passed(allowed_names, forbidden_names): 17 | """Ensures validation passes when any domain option not passed.""" 18 | validate_domain_names_options(allowed_names, forbidden_names) 19 | 20 | 21 | def test_passes_when_names_no_intersect(): 22 | """Ensures validation passes when names no intersect.""" 23 | validate_domain_names_options(['node'], ['visitor']) 24 | 25 | 26 | def test_raises_valueerror_when_names_intersect(): 27 | """Ensures `ValueError` exception is raised when names intersect.""" 28 | with pytest.raises(ValueError, match='visitor'): 29 | validate_domain_names_options(['visitor', 'handle'], ['visitor']) 30 | -------------------------------------------------------------------------------- /tests/test_regressions/test_regression112.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from pyflakes.checker import Checker as PyFlakesChecker 4 | 5 | from wemake_python_styleguide.checker import Checker 6 | 7 | code_that_breaks = ''' 8 | def current_session( 9 | telegram_id: int, 10 | for_update: bool = True, 11 | ) -> TelegramSession: 12 | """ 13 | Was triggering `AttributeError`. 14 | 15 | See: https://github.com/wemake-services/wemake-python-styleguide/issues/112 16 | """ 17 | try: 18 | query = TelegramSession.objects.all() 19 | if for_update: # Try to comment this `if` to fix everything 20 | query = query.select_for_update() 21 | 22 | return query.get( 23 | uid=telegram_id, 24 | is_verified=True, 25 | ) 26 | 27 | except TelegramSession.DoesNotExist: 28 | raise AuthenticationException('Session is missing') 29 | ''' 30 | 31 | 32 | def test_regression112(default_options): 33 | """ 34 | There was a conflict between ``pyflakes`` and our plugin. 35 | 36 | We were fighting for ``parent`` property. 37 | Now we use a custom prefix. 38 | 39 | See: https://github.com/wemake-services/wemake-python-styleguide/issues/112 40 | """ 41 | module = ast.parse(code_that_breaks) 42 | Checker.parse_options(default_options) 43 | 44 | # Now we create modifications to the tree: 45 | Checker(tree=module, file_tokens=[], filename='custom.py') 46 | 47 | # It was failing on this line: 48 | # AttributeError: 'ExceptHandler' object has no attribute 'depth' 49 | flakes = PyFlakesChecker(module) 50 | 51 | assert module.wps_context is None # augmentation happened! 52 | assert flakes.root 53 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from wemake_python_styleguide.version import pkg_name, pkg_version 4 | 5 | 6 | def test_call_flake8_version(): 7 | """Checks that module is registered and visible in the meta data.""" 8 | pkg_qualifier = pkg_name.replace('_', '-') 9 | assert pkg_qualifier 10 | assert pkg_version 11 | 12 | output_text = subprocess.check_output( 13 | ['flake8', '--version'], 14 | stderr=subprocess.STDOUT, 15 | text=True, 16 | encoding='utf8', 17 | ) 18 | output_text = output_text.replace('_', '-').replace('\n', '') 19 | 20 | assert pkg_qualifier in output_text 21 | assert pkg_version in output_text 22 | 23 | 24 | def test_call_flake8_help(): 25 | """Checks that module is registered and visible in the help.""" 26 | output_text = subprocess.check_output( 27 | ['flake8', '--help'], 28 | stderr=subprocess.STDOUT, 29 | text=True, 30 | encoding='utf8', 31 | ) 32 | 33 | assert pkg_name 34 | assert pkg_name in output_text or pkg_name.replace('_', '-') in output_text 35 | -------------------------------------------------------------------------------- /tests/test_violations/test_definition_order.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import re 3 | 4 | 5 | def _get_sorted_classes(classes): 6 | sorted_by_code = sorted(classes, key=lambda cl: cl.code) 7 | sorted_by_source = sorted( 8 | classes, 9 | key=lambda cl: inspect.findsource(cl)[1], 10 | ) 11 | 12 | return sorted_by_code, sorted_by_source 13 | 14 | 15 | def test_violation_source_order(all_module_violations): 16 | """Used to force violations order inside the source code.""" 17 | for classes in all_module_violations.values(): 18 | sorted_by_code, sorted_by_source = _get_sorted_classes(classes) 19 | 20 | assert sorted_by_code == sorted_by_source 21 | 22 | 23 | def test_violation_autoclass_order(all_module_violations): 24 | """Used to force violations order inside the `autoclass` directives.""" 25 | for module, classes in all_module_violations.items(): 26 | sorted_by_code, _ = _get_sorted_classes(classes) 27 | pattern = re.compile(r'\.\.\sautoclass::\s(\w+)') 28 | sorted_by_autoclass = pattern.findall(module.__doc__) 29 | sorted_by_code = [cl.__qualname__ for cl in sorted_by_code] 30 | 31 | assert sorted_by_code == sorted_by_autoclass 32 | -------------------------------------------------------------------------------- /tests/test_violations/test_implementation.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import re 3 | 4 | import pytest 5 | 6 | from wemake_python_styleguide.violations.base import ASTViolation 7 | 8 | 9 | class NewViolation(ASTViolation): 10 | """ 11 | Yells at cloud. 12 | 13 | Yay, I'm a docstring! 14 | """ 15 | 16 | code = 1 17 | error_template = '{0}' 18 | 19 | 20 | def test_visitor_returns_location(): 21 | """Ensures that `BaseNodeVisitor` return correct violation message.""" 22 | assert NewViolation.full_code == 'WPS001' 23 | assert NewViolation.summary == 'Yells at cloud.' 24 | 25 | visitor = NewViolation(node=ast.parse(''), text='violation') 26 | assert visitor.node_items() == (0, 0, 'WPS001 violation') 27 | 28 | 29 | def test_violation_must_have_docstring(): 30 | """Ensures that `BaseNodeVisitor` return correct violation message.""" 31 | with pytest.raises( 32 | TypeError, 33 | match=re.escape( 34 | 'Please include a docstring documenting ' 35 | + ".IShallNotPass'>", 37 | ), 38 | ): 39 | 40 | class IShallNotPass(ASTViolation): # noqa: WPS431 41 | code = 123 42 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_builtins/test_assign/test_multiple_assign.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | MultipleAssignmentsViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.builtins import ( 7 | WrongAssignmentVisitor, 8 | ) 9 | 10 | # Correct usages: 11 | 12 | single_assignment = 'constant = 1' 13 | tuple_assignment = 'first, second = (1, 2)' 14 | spread_assignment = 'first, *_, second = [1, 2, 4, 3]' 15 | 16 | # Wrong usages: 17 | 18 | two_assignment = 'first = second = 1' 19 | three_assignment = 'first = second = third' 20 | 21 | 22 | @pytest.mark.parametrize( 23 | 'code', 24 | [ 25 | single_assignment, 26 | tuple_assignment, 27 | spread_assignment, 28 | ], 29 | ) 30 | def test_correct_assignments( 31 | assert_errors, 32 | parse_ast_tree, 33 | code, 34 | default_options, 35 | ): 36 | """Testing that correct assignments work.""" 37 | tree = parse_ast_tree(code) 38 | 39 | visitor = WrongAssignmentVisitor(default_options, tree=tree) 40 | visitor.run() 41 | 42 | assert_errors(visitor, []) 43 | 44 | 45 | @pytest.mark.parametrize( 46 | 'code', 47 | [ 48 | two_assignment, 49 | three_assignment, 50 | ], 51 | ) 52 | def test_multiple_assignments( 53 | assert_errors, 54 | parse_ast_tree, 55 | code, 56 | default_options, 57 | ): 58 | """Testing that multiple assignments are restricted.""" 59 | tree = parse_ast_tree(code) 60 | 61 | visitor = WrongAssignmentVisitor(default_options, tree=tree) 62 | visitor.run() 63 | 64 | assert_errors(visitor, [MultipleAssignmentsViolation]) 65 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_exception_base_class.py: -------------------------------------------------------------------------------- 1 | from wemake_python_styleguide.violations.best_practices import ( 2 | BaseExceptionSubclassViolation, 3 | ) 4 | from wemake_python_styleguide.visitors.ast.classes.classdef import ( 5 | WrongClassDefVisitor, 6 | ) 7 | 8 | class_with_base = """ 9 | class Meta({0}): 10 | '''Docs.''' 11 | """ 12 | 13 | 14 | def test_base_exception_subclass( 15 | assert_errors, 16 | parse_ast_tree, 17 | default_options, 18 | ): 19 | """Testing that it is not possible to subclass `BaseException`.""" 20 | tree = parse_ast_tree(class_with_base.format('BaseException')) 21 | 22 | visitor = WrongClassDefVisitor(default_options, tree=tree) 23 | visitor.run() 24 | 25 | assert_errors(visitor, [BaseExceptionSubclassViolation]) 26 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_classes/test_methods/test_staticmethod.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.oop import StaticMethodViolation 4 | from wemake_python_styleguide.visitors.ast.classes.methods import ( 5 | WrongMethodVisitor, 6 | ) 7 | 8 | decorated_method = """ 9 | class Example: 10 | @{0} 11 | def should_fail(arg1): ... 12 | """ 13 | 14 | 15 | def test_staticmethod_used( 16 | assert_errors, 17 | parse_ast_tree, 18 | default_options, 19 | mode, 20 | ): 21 | """Testing that some built-in functions are restricted as decorators.""" 22 | tree = parse_ast_tree(mode(decorated_method.format('staticmethod'))) 23 | 24 | visitor = WrongMethodVisitor(default_options, tree=tree) 25 | visitor.run() 26 | 27 | assert_errors(visitor, [StaticMethodViolation]) 28 | 29 | 30 | @pytest.mark.parametrize( 31 | 'decorator', 32 | [ 33 | 'classmethod', 34 | 'custom', 35 | 'with_params(12, 100)', 36 | ], 37 | ) 38 | def test_regular_decorator_used( 39 | assert_errors, 40 | parse_ast_tree, 41 | decorator, 42 | default_options, 43 | mode, 44 | ): 45 | """Testing that other decorators are allowed.""" 46 | tree = parse_ast_tree(mode(decorated_method.format(decorator))) 47 | 48 | visitor = WrongMethodVisitor(default_options, tree=tree) 49 | visitor.run() 50 | 51 | assert_errors(visitor, []) 52 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_compares/test_float_complex_compare.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | FloatComplexCompareViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.compares import ( 7 | WrongFloatComplexCompareVisitor, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'code', 13 | [ 14 | '3.0 > 2.0 + 1.0', 15 | 'f2/f1 != 1.0', 16 | '3.0 in item_list', 17 | '2.0 < x < 3.0', 18 | '5.0 not in abc_list', 19 | '3.0 == 0.1*3', 20 | '2j != (0.2j)/0.1', 21 | '2j in comp_list', 22 | '1j != 2 != 3.0', 23 | 'x == 4.0j', 24 | ], 25 | ) 26 | def test_float_complex_compare( 27 | assert_errors, 28 | parse_ast_tree, 29 | code, 30 | default_options, 31 | ): 32 | """Testing that compares with ``float`` and ``complex`` raise violations.""" 33 | tree = parse_ast_tree(code) 34 | 35 | visitor = WrongFloatComplexCompareVisitor(default_options, tree=tree) 36 | visitor.run() 37 | 38 | assert_errors(visitor, [FloatComplexCompareViolation]) 39 | 40 | 41 | @pytest.mark.parametrize( 42 | 'code', 43 | [ 44 | 'x > 3', 45 | 'x <= y', 46 | 'abs(x - y) <= eps', 47 | 'isclose(x, 5.0)', 48 | 'isclose(y, 3j)', 49 | '3 in item_list', 50 | '3 != 5', 51 | '3.0 + x', 52 | '4.5 + y > z', 53 | ], 54 | ) 55 | def test_correct_compares( 56 | assert_errors, 57 | parse_ast_tree, 58 | code, 59 | default_options, 60 | ): 61 | """Testing safe compares.""" 62 | tree = parse_ast_tree(code) 63 | 64 | visitor = WrongFloatComplexCompareVisitor(default_options, tree=tree) 65 | visitor.run() 66 | 67 | assert_errors(visitor, []) 68 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_compares/test_heterogenous_compare.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | HeterogeneousCompareViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.compares import CompareSanityVisitor 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 'code', 11 | [ 12 | 'x > y < z', 13 | 'x >= y < z', 14 | 'x > y <= z', 15 | 'x >= y <= z', 16 | 'x < y > z', 17 | 'x <= y > z', 18 | 'x < y >= z', 19 | 'x <= y >= z', 20 | 'x > y != 0', 21 | 'x < y == 0', 22 | 'x >= y != 0', 23 | 'x <= y == 0', 24 | 'x == y != z', 25 | 'long == x == y >= z', 26 | 'call() != attr.prop in array', 27 | 'item not in array == value', 28 | ], 29 | ) 30 | def test_heterogeneous_compare( 31 | assert_errors, 32 | parse_ast_tree, 33 | code, 34 | default_options, 35 | ): 36 | """Testing that compares with different operators raise.""" 37 | tree = parse_ast_tree(code) 38 | 39 | visitor = CompareSanityVisitor(default_options, tree=tree) 40 | visitor.run() 41 | 42 | assert_errors(visitor, [HeterogeneousCompareViolation]) 43 | 44 | 45 | @pytest.mark.parametrize( 46 | 'code', 47 | [ 48 | 'x == y == z', 49 | 'z != y != x', 50 | 'call() == other.prop', 51 | 'x in y', 52 | 'x not in y', 53 | ], 54 | ) 55 | def test_correct_compare_operators( 56 | assert_errors, 57 | parse_ast_tree, 58 | code, 59 | default_options, 60 | ): 61 | """Testing that compares work well.""" 62 | tree = parse_ast_tree(code) 63 | 64 | visitor = CompareSanityVisitor(default_options, tree=tree) 65 | visitor.run() 66 | 67 | assert_errors(visitor, []) 68 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_compares/test_reversed_complex_compare.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.consistency import ( 4 | ReversedComplexCompareViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.compares import CompareSanityVisitor 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 'code', 11 | [ 12 | 'x > y >= z', 13 | 'x > y > z', 14 | ], 15 | ) 16 | def test_reversed_complex_compare( 17 | assert_errors, 18 | parse_ast_tree, 19 | code, 20 | default_options, 21 | ): 22 | """Testing that reversed compares raise a violation.""" 23 | tree = parse_ast_tree(code) 24 | 25 | visitor = CompareSanityVisitor(default_options, tree=tree) 26 | visitor.run() 27 | 28 | assert_errors(visitor, [ReversedComplexCompareViolation]) 29 | 30 | 31 | @pytest.mark.parametrize( 32 | 'code', 33 | [ 34 | 'x < y <= z', 35 | 'x < y < z', 36 | 'x <= y < z', 37 | 'x <= y <= z', 38 | 'x == y == z', 39 | 'x != y != z', 40 | ], 41 | ) 42 | def test_correct_compare( 43 | assert_errors, 44 | parse_ast_tree, 45 | code, 46 | default_options, 47 | ): 48 | """Testing that compares work well.""" 49 | tree = parse_ast_tree(code) 50 | 51 | visitor = CompareSanityVisitor(default_options, tree=tree) 52 | visitor.run() 53 | 54 | assert_errors(visitor, []) 55 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_complexity/test_call_chains/test_call_chains.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.complexity import ( 4 | TooLongCallChainViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.complexity.calls import ( 7 | CallChainsVisitor, 8 | ) 9 | 10 | # incorrect expression 11 | deep_call_chain = 'foo(a)(b)(c)(d)' 12 | 13 | # correct expression 14 | call_chain = 'bar(a)(b)' 15 | 16 | # border expression 17 | long_call_chain = 'baz(a)(b)(c)' 18 | 19 | 20 | @pytest.mark.parametrize( 21 | 'code', 22 | [ 23 | deep_call_chain, 24 | call_chain, 25 | long_call_chain, 26 | ], 27 | ) 28 | def test_correct_cases( 29 | assert_errors, 30 | parse_ast_tree, 31 | code, 32 | options, 33 | mode, 34 | ): 35 | """Testing that expressions with correct call chain length work well.""" 36 | tree = parse_ast_tree(mode(code)) 37 | 38 | option_values = options(max_call_level=4) 39 | visitor = CallChainsVisitor(option_values, tree=tree) 40 | visitor.run() 41 | 42 | assert_errors(visitor, []) 43 | 44 | 45 | @pytest.mark.parametrize( 46 | ('code', 'call_level'), 47 | [ 48 | (call_chain, 2), 49 | (deep_call_chain, 4), 50 | (long_call_chain, 3), 51 | ], 52 | ) 53 | def test_incorrect_cases( 54 | assert_errors, 55 | assert_error_text, 56 | parse_ast_tree, 57 | code, 58 | call_level, 59 | options, 60 | mode, 61 | ): 62 | """Testing that violations are raised when using a too long call chain.""" 63 | tree = parse_ast_tree(mode(code)) 64 | 65 | option_values = options(max_call_level=1) 66 | visitor = CallChainsVisitor(option_values, tree=tree) 67 | visitor.run() 68 | 69 | assert_errors(visitor, [TooLongCallChainViolation]) 70 | assert_error_text(visitor, str(call_level), option_values.max_call_level) 71 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_functions/test_implicit_primitive.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.refactoring import ( 4 | ImplicitPrimitiveViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.functions import ( 7 | UselessLambdaDefinitionVisitor, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'code', 13 | [ 14 | 'lambda x: 0', 15 | 'lambda *x: []', 16 | 'lambda **x: ()', 17 | 'lambda x, y: 0', 18 | 'lambda: 1', 19 | 'lambda x=1: 0', 20 | 'lambda: [0]', 21 | 'lambda: [some]', 22 | 'lambda: None', 23 | 'lambda: True', 24 | 'lambda: "a"', 25 | 'lambda: b"a"', 26 | 'lambda: (1, 2)', 27 | 'lambda: name', 28 | ], 29 | ) 30 | def test_correct_lambda( 31 | assert_errors, 32 | parse_ast_tree, 33 | code, 34 | default_options, 35 | ): 36 | """Testing that isinstance is callable with correct types.""" 37 | tree = parse_ast_tree(code) 38 | 39 | visitor = UselessLambdaDefinitionVisitor(default_options, tree=tree) 40 | visitor.run() 41 | 42 | assert_errors(visitor, []) 43 | 44 | 45 | @pytest.mark.parametrize( 46 | 'code', 47 | [ 48 | 'lambda: 0', 49 | 'lambda: 0.0', 50 | 'lambda: 0j', 51 | 'lambda: b""', 52 | 'lambda: ""', 53 | 'lambda: []', 54 | 'lambda: ()', 55 | 'lambda: False', 56 | 'lambda: lambda: ""', 57 | 'lambda: {}', 58 | ], 59 | ) 60 | def test_wrong_lambda( 61 | assert_errors, 62 | parse_ast_tree, 63 | code, 64 | default_options, 65 | ): 66 | """Testing that isinstance is callable with correct types.""" 67 | tree = parse_ast_tree(code) 68 | 69 | visitor = UselessLambdaDefinitionVisitor(default_options, tree=tree) 70 | visitor.run() 71 | 72 | assert_errors(visitor, [ImplicitPrimitiveViolation]) 73 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_keywords/test_context_managers/test_context_managers_definitions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | ContextManagerVariableDefinitionViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.keywords import ( 7 | WrongContextManagerVisitor, 8 | ) 9 | 10 | context_manager_definition = """ 11 | def wrapper(): 12 | with open('') as {0}: 13 | ... 14 | """ 15 | 16 | 17 | @pytest.mark.parametrize( 18 | 'code', 19 | [ 20 | 'xy[0]', 21 | 'xy.attr', 22 | 'xy["key"]', 23 | '(valid, invalid.attr)', 24 | '(invalid.attr, valid)', 25 | ], 26 | ) 27 | def test_context_manager_wrong_definitions( 28 | assert_errors, 29 | parse_ast_tree, 30 | code, 31 | default_options, 32 | mode, 33 | ): 34 | """Testing incorrect definitions context manager assignment.""" 35 | tree = parse_ast_tree(mode(context_manager_definition.format(code))) 36 | 37 | visitor = WrongContextManagerVisitor(default_options, tree=tree) 38 | visitor.run() 39 | 40 | assert_errors(visitor, [ContextManagerVariableDefinitionViolation]) 41 | 42 | 43 | @pytest.mark.parametrize( 44 | 'code', 45 | [ 46 | 'xy', 47 | '(valid1, valid2)', 48 | '(valid, *star)', 49 | '(first, second, *star)', 50 | ], 51 | ) 52 | def test_context_manager_correct_definitions( 53 | assert_errors, 54 | parse_ast_tree, 55 | code, 56 | default_options, 57 | mode, 58 | ): 59 | """Testing correct definitions context manager assignment.""" 60 | tree = parse_ast_tree(mode(context_manager_definition.format(code))) 61 | 62 | visitor = WrongContextManagerVisitor(default_options, tree=tree) 63 | visitor.run() 64 | 65 | assert_errors(visitor, []) 66 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_keywords/test_del.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | WrongKeywordViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.keywords import WrongKeywordVisitor 7 | 8 | del_variable = """ 9 | x = 5 10 | del x 11 | """ 12 | 13 | del_key = """ 14 | temp_dict = {'a': 1} 15 | del temp_dict['a'] 16 | """ 17 | 18 | del_index = """ 19 | temp_list = [1, 2, 3] 20 | del temp_list[0] 21 | """ 22 | 23 | 24 | @pytest.mark.parametrize( 25 | 'code', 26 | [ 27 | del_variable, 28 | del_key, 29 | del_index, 30 | ], 31 | ) 32 | def test_del_keyword( 33 | assert_errors, 34 | assert_error_text, 35 | parse_ast_tree, 36 | code, 37 | default_options, 38 | ): 39 | """ 40 | Testing that `del` keyword is restricted. 41 | 42 | Regression: 43 | https://github.com/wemake-services/wemake-python-styleguide/issues/493 44 | """ 45 | tree = parse_ast_tree(code) 46 | 47 | visitor = WrongKeywordVisitor(default_options, tree=tree) 48 | visitor.run() 49 | 50 | assert_errors(visitor, [WrongKeywordViolation]) 51 | assert_error_text(visitor, 'del') 52 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_keywords/test_generator_keywords/test_yield_from_type.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.consistency import ( 4 | IncorrectYieldFromTargetViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.keywords import ( 7 | GeneratorKeywordsVisitor, 8 | ) 9 | 10 | yield_from_template = """ 11 | def wrapper(): 12 | yield from {0} 13 | """ 14 | 15 | 16 | @pytest.mark.parametrize( 17 | 'code', 18 | [ 19 | '()', 20 | '[1, 2, 3]', 21 | '[name, other]', 22 | '{1, 2, 3}', 23 | '"abc"', 24 | 'b"abc"', 25 | 'a + b', 26 | '[a for a in some()]', 27 | '{a for a in some()}', 28 | ], 29 | ) 30 | def test_yield_from_incorrect_type( 31 | assert_errors, 32 | parse_ast_tree, 33 | code, 34 | default_options, 35 | ): 36 | """Ensure that `yield from` does not work with incorrect types.""" 37 | tree = parse_ast_tree(yield_from_template.format(code)) 38 | 39 | visitor = GeneratorKeywordsVisitor(default_options, tree=tree) 40 | visitor.run() 41 | 42 | assert_errors(visitor, [IncorrectYieldFromTargetViolation]) 43 | 44 | 45 | @pytest.mark.parametrize( 46 | 'code', 47 | [ 48 | 'name', 49 | 'name.attr', 50 | 'name[0]', 51 | 'name.call()', 52 | '(a for a in some())', 53 | '(1,)', 54 | '(1, 2, 3)', 55 | ], 56 | ) 57 | def test_yield_from_correct_type( 58 | assert_errors, 59 | parse_ast_tree, 60 | code, 61 | default_options, 62 | ): 63 | """Ensure that `yield from` works with correct types.""" 64 | tree = parse_ast_tree(yield_from_template.format(code)) 65 | 66 | visitor = GeneratorKeywordsVisitor(default_options, tree=tree) 67 | visitor.run() 68 | 69 | assert_errors(visitor, []) 70 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_keywords/test_raise/test_system_error_violation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.consistency import ( 4 | RaiseSystemExitViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.keywords import WrongRaiseVisitor 7 | 8 | template = 'raise {0}' 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'code', 13 | [ 14 | 'SystemExit', 15 | 'SystemExit()', 16 | 'SystemExit(0)', 17 | 'SystemExit(code)', 18 | ], 19 | ) 20 | def test_raise_system_error( 21 | assert_errors, 22 | parse_ast_tree, 23 | code, 24 | default_options, 25 | ): 26 | """Testing `raise SystemExit` is restricted.""" 27 | tree = parse_ast_tree(template.format(code)) 28 | 29 | visitor = WrongRaiseVisitor(default_options, tree=tree) 30 | visitor.run() 31 | 32 | assert_errors(visitor, [RaiseSystemExitViolation]) 33 | 34 | 35 | @pytest.mark.parametrize( 36 | 'code', 37 | [ 38 | 'NotImplementedError', 39 | 'NotImplementedError()', 40 | 'CustomSystemExit', 41 | 'CustomSystemExit()', 42 | 'custom.SystemExit', 43 | 'custom.SystemExit()', 44 | 'SystemError', 45 | ], 46 | ) 47 | def test_raise_good_errors( 48 | assert_errors, 49 | parse_ast_tree, 50 | code, 51 | default_options, 52 | ): 53 | """Testing that other exceptions are allowed.""" 54 | tree = parse_ast_tree(template.format(code)) 55 | 56 | visitor = WrongRaiseVisitor(default_options, tree=tree) 57 | visitor.run() 58 | 59 | assert_errors(visitor, []) 60 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_loops/test_loops/test_for_else.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.visitors.ast.conditions import IfStatementVisitor 4 | 5 | code_that_breaks = """ 6 | def is_prime(number: int): 7 | for i in range(2, number): 8 | if number % i == 0: 9 | return False 10 | else: 11 | if number != 1: 12 | return True 13 | return False 14 | """ 15 | 16 | code_that_works = """ 17 | def is_prime(number: int): 18 | for i in range(2, number): 19 | if number % i == 0: 20 | return False 21 | """ 22 | 23 | code_with_complex_if = """ 24 | def _has_same_args( 25 | node, 26 | call, 27 | ): 28 | for num in node: 29 | if num: 30 | if isinstance(num, int): 31 | return False 32 | elif isinstance(num, str): 33 | if num > 2: 34 | return False 35 | else: 36 | return False 37 | return True 38 | """ 39 | 40 | 41 | @pytest.mark.parametrize( 42 | 'code', 43 | [ 44 | code_that_breaks, 45 | code_that_works, 46 | code_with_complex_if, 47 | ], 48 | ) 49 | def test_regression_for_else( 50 | assert_errors, 51 | default_options, 52 | parse_ast_tree, 53 | code, 54 | ): 55 | """Tests for nested ``if`` statement in ``for else`` statement.""" 56 | tree = parse_ast_tree(code) 57 | 58 | visitor = IfStatementVisitor(default_options, tree=tree) 59 | visitor.run() 60 | 61 | assert_errors(visitor, []) 62 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_loops/test_loops/test_variable_definitions_loops.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | LoopVariableDefinitionViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.loops import ( 7 | WrongLoopDefinitionVisitor, 8 | ) 9 | 10 | for_loop_def = """ 11 | def wrapper(): 12 | for {0} in some: 13 | ... 14 | """ 15 | 16 | 17 | @pytest.mark.parametrize( 18 | 'definition', 19 | [ 20 | 'xy[0]', 21 | 'xy.attr', 22 | 'xy["key"]', 23 | '(valid, invalid.attr)', 24 | '(invalid.attr, valid)', 25 | ], 26 | ) 27 | def test_wrong_definition_loop( 28 | assert_errors, 29 | parse_ast_tree, 30 | definition, 31 | default_options, 32 | mode, 33 | ): 34 | """Ensures that wrong definitions are not allowed.""" 35 | tree = parse_ast_tree(mode(for_loop_def.format(definition))) 36 | 37 | visitor = WrongLoopDefinitionVisitor(default_options, tree=tree) 38 | visitor.run() 39 | 40 | assert_errors(visitor, [LoopVariableDefinitionViolation]) 41 | 42 | 43 | @pytest.mark.parametrize( 44 | 'definition', 45 | [ 46 | 'xy', 47 | '(valid1, valid2)', 48 | 'valid1, *valid2', 49 | ], 50 | ) 51 | def test_correct_definition_loop( 52 | assert_errors, 53 | parse_ast_tree, 54 | definition, 55 | default_options, 56 | mode, 57 | ): 58 | """Ensures that correct definitions are allowed.""" 59 | tree = parse_ast_tree(mode(for_loop_def.format(definition))) 60 | 61 | visitor = WrongLoopDefinitionVisitor(default_options, tree=tree) 62 | visitor.run() 63 | 64 | assert_errors(visitor, []) 65 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_modules/test_empty_modules.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | EmptyModuleViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.modules import ( 7 | EmptyModuleContentsVisitor, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'filename', 13 | [ 14 | 'empty.py', 15 | '/home/user/logic.py', 16 | 'partial/views.py', 17 | 'C:/path/package/module.py', 18 | ], 19 | ) 20 | def test_simple_filename( 21 | assert_errors, 22 | parse_ast_tree, 23 | filename, 24 | default_options, 25 | ): 26 | """Testing that simple file names should not be empty.""" 27 | tree = parse_ast_tree('') 28 | 29 | visitor = EmptyModuleContentsVisitor( 30 | default_options, 31 | tree=tree, 32 | filename=filename, 33 | ) 34 | visitor.run() 35 | 36 | assert_errors(visitor, [EmptyModuleViolation]) 37 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_modules/test_magic_module_functions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.constants import MAGIC_MODULE_NAMES_BLACKLIST 4 | from wemake_python_styleguide.violations.best_practices import ( 5 | BadMagicModuleFunctionViolation, 6 | ) 7 | from wemake_python_styleguide.visitors.ast.modules import ( 8 | MagicModuleFunctionsVisitor, 9 | ) 10 | 11 | module_level_method = """ 12 | def {0}(name): 13 | ... 14 | """ 15 | 16 | class_level_method = """ 17 | class Example: 18 | def {0}(name): 19 | ... 20 | """ 21 | 22 | 23 | @pytest.mark.parametrize( 24 | 'code', 25 | [ 26 | module_level_method, 27 | ], 28 | ) 29 | @pytest.mark.parametrize('function_names', MAGIC_MODULE_NAMES_BLACKLIST) 30 | def test_wrong_magic_used( 31 | assert_errors, 32 | code, 33 | parse_ast_tree, 34 | default_options, 35 | function_names, 36 | ): 37 | """Testing that some magic methods are restricted.""" 38 | tree = parse_ast_tree(code.format(function_names)) 39 | 40 | visitor = MagicModuleFunctionsVisitor(default_options, tree=tree) 41 | visitor.run() 42 | 43 | assert_errors(visitor, [BadMagicModuleFunctionViolation]) 44 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_naming/test_first_arguments.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.constants import SPECIAL_ARGUMENT_NAMES_WHITELIST 4 | from wemake_python_styleguide.visitors.ast.naming.validation import ( 5 | WrongNameVisitor, 6 | ) 7 | 8 | lambda_first_argument = 'lambda {0}: ...' 9 | function_first_argument = 'def function({0}): ...' 10 | 11 | method_first_argument = """ 12 | class Test: 13 | def method({0}): ... 14 | """ 15 | 16 | classmethod_first_argument = """ 17 | class Test: 18 | @classmethod 19 | def method({0}): ... 20 | """ 21 | 22 | meta_first_argument = """ 23 | class Test(type): 24 | def __new__({0}): ... 25 | """ 26 | 27 | 28 | @pytest.mark.parametrize('argument', SPECIAL_ARGUMENT_NAMES_WHITELIST) 29 | @pytest.mark.parametrize( 30 | 'code', 31 | [ 32 | function_first_argument, 33 | method_first_argument, 34 | classmethod_first_argument, 35 | meta_first_argument, 36 | ], 37 | ) 38 | def test_correct_first_arguments( 39 | assert_errors, 40 | parse_ast_tree, 41 | argument, 42 | code, 43 | default_options, 44 | mode, 45 | ): 46 | """Testing that first arguments are allowed.""" 47 | tree = parse_ast_tree(mode(code.format(argument))) 48 | 49 | visitor = WrongNameVisitor(default_options, tree=tree) 50 | visitor.run() 51 | 52 | assert_errors(visitor, []) 53 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_naming/test_naming_rules/test_consecutive_underscore.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.naming import ( 4 | ConsecutiveUnderscoresInNameViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.naming.validation import ( 7 | WrongNameVisitor, 8 | ) 9 | 10 | patterns = ( 11 | 'with__underscore', 12 | 'mutliple__under__score', 13 | 'triple___underscore', 14 | '__magic__name__', 15 | ) 16 | 17 | 18 | @pytest.mark.parametrize('underscored_name', patterns) 19 | def test_underscored_variable_name( 20 | assert_errors, 21 | assert_error_text, 22 | parse_ast_tree, 23 | own_naming_template, 24 | default_options, 25 | mode, 26 | underscored_name, 27 | ): 28 | """Ensures that underscored names are not allowed.""" 29 | tree = parse_ast_tree( 30 | mode(own_naming_template.format(underscored_name)), 31 | ) 32 | 33 | visitor = WrongNameVisitor(default_options, tree=tree) 34 | visitor.run() 35 | 36 | assert_errors(visitor, [ConsecutiveUnderscoresInNameViolation]) 37 | assert_error_text(visitor, underscored_name) 38 | 39 | 40 | @pytest.mark.parametrize('underscored_name', patterns) 41 | def test_underscored_attribute_name( 42 | assert_errors, 43 | parse_ast_tree, 44 | foreign_naming_template, 45 | default_options, 46 | mode, 47 | underscored_name, 48 | ): 49 | """Ensures that attribute underscored names are allowed.""" 50 | tree = parse_ast_tree( 51 | mode(foreign_naming_template.format(underscored_name)), 52 | ) 53 | 54 | visitor = WrongNameVisitor(default_options, tree=tree) 55 | visitor.run() 56 | 57 | assert_errors(visitor, []) 58 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_naming/test_naming_rules/test_correct.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.visitors.ast.naming.validation import ( 4 | WrongNameVisitor, 5 | ) 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 'correct_name', 10 | [ 11 | 'snake_case', 12 | '_protected', 13 | 'with_number5', 14 | ], 15 | ) 16 | def test_naming_correct( 17 | assert_errors, 18 | parse_ast_tree, 19 | naming_template, 20 | default_options, 21 | mode, 22 | correct_name, 23 | ): 24 | """Ensures that correct names are allowed.""" 25 | tree = parse_ast_tree(mode(naming_template.format(correct_name))) 26 | 27 | visitor = WrongNameVisitor(default_options, tree=tree) 28 | visitor.run() 29 | 30 | assert_errors(visitor, []) 31 | 32 | 33 | @pytest.mark.parametrize( 34 | 'allowed_name', 35 | [ 36 | 'item', 37 | 'items', 38 | 'handle', 39 | 'other_name', # unknown values are ignored silently 40 | ], 41 | ) 42 | def test_name_in_allowed_domain_names_option( 43 | assert_errors, 44 | parse_ast_tree, 45 | naming_template, 46 | options, 47 | mode, 48 | allowed_name, 49 | ): 50 | """Ensures that names listed in `allowed-domain-names` are allowed.""" 51 | tree = parse_ast_tree(mode(naming_template.format(allowed_name))) 52 | 53 | visitor = WrongNameVisitor( 54 | options(allowed_domain_names=(allowed_name,)), 55 | tree=tree, 56 | ) 57 | visitor.run() 58 | assert_errors(visitor, []) 59 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_naming/test_naming_rules/test_long.py: -------------------------------------------------------------------------------- 1 | from wemake_python_styleguide.violations.naming import TooLongNameViolation 2 | from wemake_python_styleguide.visitors.ast.naming.validation import ( 3 | WrongNameVisitor, 4 | ) 5 | 6 | long_name = 'incredibly_long_name_that_should_not_pass_the_long_name_test' 7 | 8 | 9 | def test_long_variable_name( 10 | assert_errors, 11 | assert_error_text, 12 | parse_ast_tree, 13 | own_naming_template, 14 | default_options, 15 | mode, 16 | ): 17 | """Ensures that long names are not allowed.""" 18 | tree = parse_ast_tree( 19 | mode(own_naming_template.format(long_name)), 20 | ) 21 | 22 | visitor = WrongNameVisitor(default_options, tree=tree) 23 | visitor.run() 24 | 25 | assert_errors(visitor, [TooLongNameViolation]) 26 | assert_error_text(visitor, long_name, default_options.max_name_length) 27 | 28 | 29 | def test_long_foreign_name( 30 | assert_errors, 31 | parse_ast_tree, 32 | foreign_naming_template, 33 | default_options, 34 | mode, 35 | ): 36 | """Ensures that long names are not allowed.""" 37 | tree = parse_ast_tree( 38 | mode(foreign_naming_template.format(long_name)), 39 | ) 40 | 41 | visitor = WrongNameVisitor(default_options, tree=tree) 42 | visitor.run() 43 | 44 | assert_errors(visitor, []) 45 | 46 | 47 | def test_long_variable_name_config( 48 | assert_errors, 49 | parse_ast_tree, 50 | own_naming_template, 51 | options, 52 | mode, 53 | ): 54 | """Ensures that it is possible to configure `max_name_length`.""" 55 | tree = parse_ast_tree( 56 | mode(own_naming_template.format(long_name)), 57 | ) 58 | 59 | option_values = options(max_name_length=len(long_name) + 1) 60 | visitor = WrongNameVisitor(option_values, tree=tree) 61 | visitor.run() 62 | 63 | assert_errors(visitor, []) 64 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_naming/test_naming_rules/test_private.py: -------------------------------------------------------------------------------- 1 | from wemake_python_styleguide.violations.naming import PrivateNameViolation 2 | from wemake_python_styleguide.visitors.ast.naming.validation import ( 3 | WrongNameVisitor, 4 | ) 5 | 6 | 7 | def test_private_variable_name( 8 | assert_errors, 9 | assert_error_text, 10 | parse_ast_tree, 11 | naming_template, 12 | default_options, 13 | mode, 14 | ): 15 | """Ensures that private names are not allowed.""" 16 | private_name = '__private' 17 | tree = parse_ast_tree(mode(naming_template.format(private_name))) 18 | 19 | visitor = WrongNameVisitor(default_options, tree=tree) 20 | visitor.run() 21 | 22 | assert_errors(visitor, [PrivateNameViolation]) 23 | assert_error_text(visitor, private_name) 24 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_naming/test_naming_rules/test_redability.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide import constants 4 | from wemake_python_styleguide.logic.naming.alphabet import ( 5 | get_unreadable_characters, 6 | ) 7 | from wemake_python_styleguide.violations.naming import ( 8 | UnreadableNameViolation, 9 | UpperCaseAttributeViolation, 10 | ) 11 | from wemake_python_styleguide.visitors.ast.naming.validation import ( 12 | WrongNameVisitor, 13 | ) 14 | 15 | 16 | @pytest.mark.parametrize( 17 | 'expression', 18 | [ 19 | 'My1Item', 20 | 'Element0Operation', 21 | 'O0S', 22 | 'S0O', 23 | ], 24 | ) 25 | def test_unreadable_name( 26 | assert_errors, 27 | assert_error_text, 28 | parse_ast_tree, 29 | own_naming_template, 30 | expression, 31 | default_options, 32 | mode, 33 | ): 34 | """Ensures that unreadable names are not allowed.""" 35 | tree = parse_ast_tree( 36 | mode(own_naming_template.format(expression, expression)), 37 | ) 38 | 39 | visitor = WrongNameVisitor(default_options, tree=tree) 40 | visitor.run() 41 | 42 | unreadable = get_unreadable_characters( 43 | expression, 44 | constants.UNREADABLE_CHARACTER_COMBINATIONS, 45 | ) 46 | 47 | assert_errors( 48 | visitor, 49 | [UnreadableNameViolation], 50 | ignored_types=UpperCaseAttributeViolation, 51 | ) 52 | assert_error_text( 53 | visitor, 54 | unreadable, 55 | ignored_types=UpperCaseAttributeViolation, 56 | ) 57 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_naming/test_naming_rules/test_underscored_number.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.naming import ( 4 | UnderscoredNumberNameViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.naming.validation import ( 7 | WrongNameVisitor, 8 | ) 9 | 10 | patterns = ( 11 | 'number_5', 12 | 'between_45_letters', 13 | 'with_multiple_groups_4_5', 14 | ) 15 | 16 | 17 | @pytest.mark.parametrize('number_suffix', patterns) 18 | def test_number_prefix_variable_name( 19 | assert_errors, 20 | assert_error_text, 21 | parse_ast_tree, 22 | own_naming_template, 23 | default_options, 24 | mode, 25 | number_suffix, 26 | ): 27 | """Ensures that number suffix names are not allowed.""" 28 | tree = parse_ast_tree( 29 | mode(own_naming_template.format(number_suffix)), 30 | ) 31 | 32 | visitor = WrongNameVisitor(default_options, tree=tree) 33 | visitor.run() 34 | 35 | assert_errors(visitor, [UnderscoredNumberNameViolation]) 36 | assert_error_text(visitor, number_suffix) 37 | 38 | 39 | @pytest.mark.parametrize('number_suffix', patterns) 40 | def test_number_prefix_foreign_name( 41 | assert_errors, 42 | parse_ast_tree, 43 | foreign_naming_template, 44 | default_options, 45 | mode, 46 | number_suffix, 47 | ): 48 | """Ensures that number suffix names are not allowed.""" 49 | tree = parse_ast_tree( 50 | mode(foreign_naming_template.format(number_suffix)), 51 | ) 52 | 53 | visitor = WrongNameVisitor(default_options, tree=tree) 54 | visitor.run() 55 | 56 | assert_errors(visitor, []) 57 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_naming/test_naming_rules/test_wrong_unused_name.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.naming import ( 4 | WrongUnusedVariableNameViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.naming.validation import ( 7 | WrongNameVisitor, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'wrong_name', 13 | [ 14 | '__', 15 | '___', 16 | ], 17 | ) 18 | def test_wrong_unused_variable_name( 19 | assert_errors, 20 | assert_error_text, 21 | parse_ast_tree, 22 | naming_template, 23 | default_options, 24 | mode, 25 | wrong_name, 26 | ): 27 | """Ensures that wrong names are not allowed.""" 28 | tree = parse_ast_tree(mode(naming_template.format(wrong_name))) 29 | 30 | visitor = WrongNameVisitor(default_options, tree=tree) 31 | visitor.run() 32 | 33 | assert_errors(visitor, [WrongUnusedVariableNameViolation]) 34 | assert_error_text(visitor, wrong_name) 35 | 36 | 37 | @pytest.mark.parametrize( 38 | 'wrong_name', 39 | [ 40 | '_', 41 | ], 42 | ) 43 | def test_correct_unused_variable_name( 44 | assert_errors, 45 | parse_ast_tree, 46 | naming_template, 47 | skip_match_case_syntax_error, 48 | default_options, 49 | mode, 50 | wrong_name, 51 | ): 52 | """Ensures that wrong names are not allowed.""" 53 | skip_match_case_syntax_error(naming_template, wrong_name) 54 | tree = parse_ast_tree(mode(naming_template.format(wrong_name))) 55 | 56 | visitor = WrongNameVisitor(default_options, tree=tree) 57 | visitor.run() 58 | 59 | assert_errors(visitor, []) 60 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_operators/test_list_multiply.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | ListMultiplyViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.operators import ( 7 | WrongMathOperatorVisitor, 8 | ) 9 | 10 | usage_template = 'constant = {0}' 11 | 12 | 13 | @pytest.mark.parametrize( 14 | 'expression', 15 | [ 16 | '[] * 1', 17 | '[1] * 2', 18 | '[1, 2] * 0', 19 | '[x for x in ()] * 1.1', 20 | '[[] * 2 for x in some]', 21 | ], 22 | ) 23 | def test_list_mult_operation( 24 | assert_errors, 25 | parse_ast_tree, 26 | expression, 27 | default_options, 28 | ): 29 | """Testing that list multiplies are forbidden.""" 30 | tree = parse_ast_tree(usage_template.format(expression)) 31 | 32 | visitor = WrongMathOperatorVisitor(default_options, tree=tree) 33 | visitor.run() 34 | 35 | assert_errors(visitor, [ListMultiplyViolation]) 36 | 37 | 38 | @pytest.mark.parametrize( 39 | 'expression', 40 | [ 41 | '1 * 2', 42 | '() * 1', 43 | '[1, 2] + [3, 4]', 44 | '[x * 1 for x in some]', 45 | '[x * 1 for x in some] + [2 * x for x in some]', 46 | ], 47 | ) 48 | def test_correct_list_operation( 49 | assert_errors, 50 | parse_ast_tree, 51 | expression, 52 | default_options, 53 | ): 54 | """Testing that non lists are allowed.""" 55 | tree = parse_ast_tree(usage_template.format(expression)) 56 | 57 | visitor = WrongMathOperatorVisitor(default_options, tree=tree) 58 | visitor.run() 59 | 60 | assert_errors(visitor, []) 61 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_pm/test_extra_subject_syntax.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.refactoring import ( 4 | ExtraMatchSubjectSyntaxViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.pm import MatchSubjectVisitor 7 | 8 | template = """ 9 | match {0}: 10 | case SomeClass(): 11 | my_print('first') 12 | """ 13 | 14 | 15 | @pytest.mark.parametrize( 16 | 'code', 17 | [ 18 | '[Some()]', 19 | '[first, second]', 20 | '{var, other}', 21 | '{test : result}', 22 | '{test : "value"}', 23 | '(one,)', 24 | ], 25 | ) 26 | def test_wrong_usage_of_subjects( 27 | assert_errors, 28 | parse_ast_tree, 29 | code, 30 | default_options, 31 | ): 32 | """Ensures that extra syntax in subjects are forbidden.""" 33 | tree = parse_ast_tree(template.format(code)) 34 | 35 | visitor = MatchSubjectVisitor(default_options, tree=tree) 36 | visitor.run() 37 | 38 | assert_errors(visitor, [ExtraMatchSubjectSyntaxViolation]) 39 | 40 | 41 | @pytest.mark.parametrize( 42 | 'code', 43 | [ 44 | 'first', 45 | 'call()', 46 | 'attr.value', 47 | '(first, second)', 48 | '(many, items, here)', 49 | # Will raise another violation: 50 | '[1, 2]', 51 | '()', 52 | '[]', 53 | ], 54 | ) 55 | def test_correct_usage_of_subjects( 56 | assert_errors, 57 | parse_ast_tree, 58 | code, 59 | default_options, 60 | ): 61 | """Ensures that it is possible to have correct subjects.""" 62 | tree = parse_ast_tree(template.format(code)) 63 | 64 | visitor = MatchSubjectVisitor(default_options, tree=tree) 65 | visitor.run() 66 | 67 | assert_errors(visitor, []) 68 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_statements/test_almost_swapped.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.refactoring import ( 4 | AlmostSwappedViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.statements import ( 7 | StatementsWithBodiesVisitor, 8 | ) 9 | 10 | # Correct: 11 | 12 | correct_swapped_variables = 'a, b = b, a' 13 | 14 | # Wrong: 15 | 16 | wrong_swapped_variables = """ 17 | a = b 18 | b = a 19 | """ 20 | 21 | wrong_swapped_uops_variables = """ 22 | a = -b 23 | b = a 24 | """ 25 | 26 | wrong_swapped_variables_with_temp = """ 27 | temp = a 28 | a = b 29 | b = temp 30 | """ 31 | 32 | wrong_double_swap = """ 33 | dx, dy = dy, dx 34 | dx, dy = dy, dx 35 | """ 36 | 37 | 38 | @pytest.mark.parametrize( 39 | 'code', 40 | [ 41 | wrong_swapped_variables, 42 | wrong_swapped_uops_variables, 43 | wrong_swapped_variables_with_temp, 44 | wrong_double_swap, 45 | ], 46 | ) 47 | def test_wrong_swapped_variables( 48 | assert_errors, 49 | parse_ast_tree, 50 | code, 51 | default_options, 52 | ): 53 | """Ensures that incorrectly swapped variables are forbidden.""" 54 | tree = parse_ast_tree(code) 55 | 56 | visitor = StatementsWithBodiesVisitor(default_options, tree=tree) 57 | visitor.run() 58 | 59 | assert_errors(visitor, [AlmostSwappedViolation]) 60 | 61 | 62 | @pytest.mark.parametrize( 63 | 'code', 64 | [ 65 | correct_swapped_variables, 66 | ], 67 | ) 68 | def test_correct_swapped_variables( 69 | assert_errors, 70 | parse_ast_tree, 71 | code, 72 | default_options, 73 | ): 74 | """Testing that correctly swapped variables.""" 75 | tree = parse_ast_tree(code) 76 | 77 | visitor = StatementsWithBodiesVisitor(default_options, tree=tree) 78 | visitor.run() 79 | 80 | assert_errors(visitor, []) 81 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_statements/test_misrefactored_assignment.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.refactoring import ( 4 | MisrefactoredAssignmentViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.statements import ( 7 | StatementsWithBodiesVisitor, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'code', 13 | [ 14 | 'x += x + 2', 15 | 'x -= x - 1', 16 | 'x *= x * 1', 17 | 'x /= x / 1', 18 | 'x **= x ** 1', 19 | 'x ^= x ^ 1', 20 | 'x %= x % 1', 21 | 'x >>= x >> 1', 22 | 'x <<= x << 1', 23 | 'x &= x & 1', 24 | 'x |= x | 1', 25 | 'x @= x | 1', 26 | ], 27 | ) 28 | def test_misrefactored_assignment( 29 | assert_errors, 30 | parse_ast_tree, 31 | code, 32 | default_options, 33 | ): 34 | """Testing that misrefactored assignments detected.""" 35 | tree = parse_ast_tree(code) 36 | 37 | visitor = StatementsWithBodiesVisitor(default_options, tree=tree) 38 | visitor.run() 39 | 40 | assert_errors(visitor, [MisrefactoredAssignmentViolation]) 41 | 42 | 43 | @pytest.mark.parametrize( 44 | 'code', 45 | [ 46 | 'x += y + 2', 47 | 'x -= 1 - y', 48 | 'x *= x2 * 1', 49 | 'x2 /= x / 1', 50 | 'x **= (x - y) ** 1', 51 | 'x ^= (x + 1) ^ 1', 52 | 'x %= x() % 1', 53 | 'x >>= a.x >> 1', 54 | 'x <<= x.x << 1', 55 | 'x &= x.x() & 1', 56 | 'x |= (y := 2) | 1', 57 | 'x |= -x | 1', 58 | ], 59 | ) 60 | def test_correct_assignment( 61 | assert_errors, 62 | parse_ast_tree, 63 | code, 64 | default_options, 65 | ): 66 | """Testing that correct assignments are possible.""" 67 | tree = parse_ast_tree(code) 68 | 69 | visitor = StatementsWithBodiesVisitor(default_options, tree=tree) 70 | visitor.run() 71 | 72 | assert_errors(visitor, []) 73 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_subscripts/test_float_key_usage.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import FloatKeyViolation 4 | from wemake_python_styleguide.visitors.ast.subscripts import CorrectKeyVisitor 5 | 6 | usage_template = 'some_dict[{0}]' 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 'expression', 11 | [ 12 | '1.0', 13 | '-0.0', 14 | '+3.5', 15 | ], 16 | ) 17 | def test_float_key_usage( 18 | assert_errors, 19 | parse_ast_tree, 20 | expression, 21 | default_options, 22 | ): 23 | """Testing that redundant subscripts are forbidden.""" 24 | tree = parse_ast_tree(usage_template.format(expression)) 25 | 26 | visitor = CorrectKeyVisitor(default_options, tree=tree) 27 | visitor.run() 28 | 29 | assert_errors(visitor, [FloatKeyViolation]) 30 | 31 | 32 | @pytest.mark.parametrize( 33 | 'expression', 34 | [ 35 | '5', 36 | 'name', 37 | 'call()', 38 | 'name.attr', 39 | 'name[sub]', 40 | '...', 41 | '"str"', 42 | 'b""', 43 | '3j', 44 | '5 + 0.1', 45 | '3 / 2', 46 | ], 47 | ) 48 | def test_correct_subscripts( 49 | assert_errors, 50 | parse_ast_tree, 51 | expression, 52 | default_options, 53 | ): 54 | """Testing that non-redundant subscripts are allowed.""" 55 | tree = parse_ast_tree(usage_template.format(expression)) 56 | 57 | visitor = CorrectKeyVisitor(default_options, tree=tree) 58 | visitor.run() 59 | 60 | assert_errors(visitor, []) 61 | -------------------------------------------------------------------------------- /tests/test_visitors/test_ast/test_subscripts/test_slice_assignment.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.consistency import ( 4 | AssignToSliceViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.ast.subscripts import SubscriptVisitor 7 | 8 | usage_template = 'constant[{0}] = [1, 2, 3]' 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'expression', 13 | [ 14 | ':7', 15 | '1:7:2', 16 | '3:', 17 | '3::', 18 | '::2', 19 | ':2:', 20 | ':', 21 | 'slice(1)', 22 | 'slice()', 23 | 'slice(1, 3)', 24 | ], 25 | ) 26 | def test_slice_assignment( 27 | assert_errors, 28 | parse_ast_tree, 29 | expression, 30 | default_options, 31 | ): 32 | """Testing that slice assignments are forbidden.""" 33 | tree = parse_ast_tree(usage_template.format(expression)) 34 | 35 | visitor = SubscriptVisitor(default_options, tree=tree) 36 | visitor.run() 37 | 38 | assert_errors(visitor, [AssignToSliceViolation]) 39 | 40 | 41 | @pytest.mark.parametrize( 42 | 'expression', 43 | [ 44 | '5', 45 | '"string"', 46 | 'object', 47 | 'dict[key]', 48 | 'subslice[1:2]', 49 | 'func(1, 2)', 50 | ], 51 | ) 52 | def test_regular_index_assignment( 53 | assert_errors, 54 | parse_ast_tree, 55 | expression, 56 | default_options, 57 | ): 58 | """Testing that regular index assignment is allowed.""" 59 | tree = parse_ast_tree(usage_template.format(expression)) 60 | 61 | visitor = SubscriptVisitor(default_options, tree=tree) 62 | visitor.run() 63 | 64 | assert_errors(visitor, []) 65 | -------------------------------------------------------------------------------- /tests/test_visitors/test_base.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | from wemake_python_styleguide import constants 4 | from wemake_python_styleguide.visitors.base import BaseFilenameVisitor 5 | 6 | 7 | class _TestingFilenameVisitor(BaseFilenameVisitor): 8 | def visit_filename(self): 9 | """Overridden to satisfy abstract base class.""" 10 | 11 | 12 | def test_base_filename_run_do_not_call_visit(default_options): 13 | """Ensures that `run()` does not call `visit()` method for stdin.""" 14 | instance = _TestingFilenameVisitor( 15 | default_options, 16 | filename=constants.STDIN, 17 | ) 18 | instance.visit_filename = MagicMock() 19 | instance.run() 20 | 21 | instance.visit_filename.assert_not_called() 22 | -------------------------------------------------------------------------------- /tests/test_visitors/test_decorators/test_alias_decorator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.visitors.decorators import alias 4 | 5 | 6 | class _HasAliasedProp: 7 | def existing(self): 8 | """Existing.""" 9 | 10 | def first(self): 11 | """First.""" 12 | 13 | 14 | def test_raises_for_duplicates(): 15 | """Ensures that decorator raises an exception for duplicates.""" 16 | with pytest.raises(ValueError, match='duplicate'): 17 | alias('name', ('duplicate', 'duplicate')) 18 | 19 | 20 | def test_useless_alias(): 21 | """Ensures that decorator raises an exception for useless alias.""" 22 | with pytest.raises(ValueError, match='duplicate'): 23 | alias('name', ('name',)) 24 | 25 | 26 | def test_raises_for_missing_alias(): 27 | """Ensures that decorator raises an exception for missing alias.""" 28 | with pytest.raises(AttributeError): 29 | alias('new_alias', ('first', 'second'))(_HasAliasedProp) 30 | 31 | 32 | def test_raises_for_existing_alias(): 33 | """Ensures that decorator raises an exception for existing alias.""" 34 | with pytest.raises(AttributeError): 35 | alias('existing', ('first', 'second'))(_HasAliasedProp) 36 | -------------------------------------------------------------------------------- /tests/test_visitors/test_filenames/test_module/test_module_magic_name.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.constants import MAGIC_MODULE_NAMES_WHITELIST 4 | from wemake_python_styleguide.violations.naming import ( 5 | WrongModuleMagicNameViolation, 6 | ) 7 | from wemake_python_styleguide.visitors.filenames.module import ( 8 | WrongModuleNameVisitor, 9 | ) 10 | 11 | 12 | @pytest.mark.parametrize('filename', MAGIC_MODULE_NAMES_WHITELIST) 13 | def test_correct_magic_filename(assert_errors, filename, default_options): 14 | """Testing that allowed magic file names works well.""" 15 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 16 | visitor.run() 17 | 18 | assert_errors(visitor, []) 19 | 20 | 21 | @pytest.mark.parametrize( 22 | 'filename', 23 | [ 24 | '__version__.py', 25 | '__custom__.py', 26 | '__some_extra__.py', 27 | ], 28 | ) 29 | def test_simple_filename( 30 | assert_errors, 31 | filename, 32 | default_options, 33 | ): 34 | """Testing that some file names are restricted.""" 35 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 36 | visitor.run() 37 | 38 | assert_errors(visitor, [WrongModuleMagicNameViolation]) 39 | -------------------------------------------------------------------------------- /tests/test_visitors/test_filenames/test_module/test_module_name.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.constants import MODULE_NAMES_BLACKLIST 4 | from wemake_python_styleguide.violations.naming import WrongModuleNameViolation 5 | from wemake_python_styleguide.visitors.filenames.module import ( 6 | WrongModuleNameVisitor, 7 | ) 8 | 9 | 10 | @pytest.mark.parametrize( 11 | 'filename', 12 | [ 13 | 'query.py', 14 | '/home/user/logic.py', 15 | 'partial/views.py', 16 | 'C:/path/package/module.py', 17 | ], 18 | ) 19 | def test_simple_filename(assert_errors, filename, default_options): 20 | """Testing that simple file names works well.""" 21 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 22 | visitor.run() 23 | 24 | assert_errors(visitor, []) 25 | 26 | 27 | @pytest.mark.parametrize('filename', MODULE_NAMES_BLACKLIST) 28 | def test_restricted_filename( 29 | assert_errors, 30 | filename, 31 | default_options, 32 | ): 33 | """Testing that some file names are restricted.""" 34 | visitor = WrongModuleNameVisitor( 35 | default_options, 36 | filename=f'{filename}.py', 37 | ) 38 | visitor.run() 39 | 40 | assert_errors(visitor, [WrongModuleNameViolation]) 41 | -------------------------------------------------------------------------------- /tests/test_visitors/test_filenames/test_module/test_module_pattern.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.naming import ( 4 | WrongModuleNamePatternViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.filenames.module import ( 7 | WrongModuleNameVisitor, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'filename', 13 | [ 14 | 'my_module.py', 15 | '_prefixed.py', 16 | '_prefixed_with_number2.py', 17 | 'regression123.py', 18 | ], 19 | ) 20 | def test_simple_filename(assert_errors, filename, default_options): 21 | """Testing that simple file names works well.""" 22 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 23 | visitor.run() 24 | 25 | assert_errors(visitor, []) 26 | 27 | 28 | @pytest.mark.parametrize( 29 | 'filename', 30 | [ 31 | 'ending_.py', 32 | 'MyModule.py', 33 | '1python.py', 34 | 'some_More.py', 35 | 'wrong+char.py', 36 | ], 37 | ) 38 | def test_wrong_filename( 39 | assert_errors, 40 | filename, 41 | default_options, 42 | ): 43 | """Testing that incorrect names are restricted.""" 44 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 45 | visitor.run() 46 | 47 | assert_errors(visitor, [WrongModuleNamePatternViolation]) 48 | -------------------------------------------------------------------------------- /tests/test_visitors/test_filenames/test_module/test_module_redable_name.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.naming import ( 4 | UnderscoredNumberNameViolation, 5 | UnreadableNameViolation, 6 | WrongModuleNamePatternViolation, 7 | ) 8 | from wemake_python_styleguide.visitors.filenames.module import ( 9 | WrongModuleNameVisitor, 10 | ) 11 | 12 | 13 | @pytest.mark.parametrize( 14 | 'filename', 15 | [ 16 | 'the1long', 17 | ], 18 | ) 19 | def test_unreadable_filename(assert_errors, filename, default_options): 20 | """Testing that unreadable characters combinations do not allowed.""" 21 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 22 | visitor.run() 23 | 24 | assert_errors(visitor, [UnreadableNameViolation]) 25 | 26 | 27 | @pytest.mark.parametrize( 28 | 'filename', 29 | [ 30 | 'ordinary', 31 | 'first_module', 32 | ], 33 | ) 34 | def test_readable_filename(assert_errors, filename, default_options): 35 | """Testing that ordinary naming works well.""" 36 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 37 | visitor.run() 38 | 39 | assert_errors(visitor, []) 40 | 41 | 42 | @pytest.mark.parametrize( 43 | 'filename', 44 | [ 45 | 'TestO_0', 46 | ], 47 | ) 48 | def test_corner_case(assert_errors, filename, default_options): 49 | """Testing corner case related to underscore name patterns.""" 50 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 51 | visitor.run() 52 | 53 | assert_errors( 54 | visitor, 55 | [ 56 | WrongModuleNamePatternViolation, 57 | UnderscoredNumberNameViolation, 58 | ], 59 | ) 60 | -------------------------------------------------------------------------------- /tests/test_visitors/test_filenames/test_module/test_module_underscore_number.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.naming import ( 4 | UnderscoredNumberNameViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.filenames.module import ( 7 | WrongModuleNameVisitor, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'filename', 13 | [ 14 | 'version_2.py', 15 | 'custom_2_version.py', 16 | ], 17 | ) 18 | def test_filename_with_underscored_number( 19 | assert_errors, 20 | assert_error_text, 21 | filename, 22 | default_options, 23 | ): 24 | """Testing that file names with underscored numbers are restricted.""" 25 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 26 | visitor.run() 27 | 28 | assert_errors(visitor, [UnderscoredNumberNameViolation]) 29 | assert_error_text(visitor, filename.replace('.py', '')) 30 | 31 | 32 | @pytest.mark.parametrize( 33 | 'filename', 34 | [ 35 | 'version2.py', 36 | 'version2_8.py', 37 | ], 38 | ) 39 | def test_filename_without_underscored_number( 40 | assert_errors, 41 | filename, 42 | default_options, 43 | ): 44 | """Testing that file names with underscored numbers are restricted.""" 45 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 46 | visitor.run() 47 | 48 | assert_errors(visitor, []) 49 | -------------------------------------------------------------------------------- /tests/test_visitors/test_filenames/test_module/test_module_underscores.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.naming import ( 4 | ConsecutiveUnderscoresInNameViolation, 5 | PrivateNameViolation, 6 | ) 7 | from wemake_python_styleguide.visitors.filenames.module import ( 8 | WrongModuleNameVisitor, 9 | ) 10 | 11 | 12 | @pytest.mark.parametrize( 13 | 'filename', 14 | [ 15 | 'some.py', 16 | 'my_module.py', 17 | '__init__.py', 18 | '_compat.py', 19 | ], 20 | ) 21 | def test_correct_filename(assert_errors, filename, default_options): 22 | """Testing that correct file names are allowed.""" 23 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 24 | visitor.run() 25 | 26 | assert_errors(visitor, []) 27 | 28 | 29 | @pytest.mark.parametrize( 30 | 'filename', 31 | [ 32 | 'compat__.py', 33 | 'some__typo.py', 34 | ], 35 | ) 36 | def test_underscore_filename( 37 | assert_errors, 38 | assert_error_text, 39 | filename, 40 | default_options, 41 | ): 42 | """Ensures incorrect underscores are caught.""" 43 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 44 | visitor.run() 45 | 46 | assert_errors(visitor, [ConsecutiveUnderscoresInNameViolation]) 47 | assert_error_text(visitor, filename.replace('.py', '')) 48 | 49 | 50 | @pytest.mark.parametrize( 51 | 'filename', 52 | [ 53 | '__private.py', 54 | '__compat_name.py', 55 | ], 56 | ) 57 | def test_private_filename( 58 | assert_errors, 59 | assert_error_text, 60 | filename, 61 | default_options, 62 | ): 63 | """Ensures that names with private names are caught.""" 64 | visitor = WrongModuleNameVisitor(default_options, filename=filename) 65 | visitor.run() 66 | 67 | assert_errors(visitor, [PrivateNameViolation]) 68 | assert_error_text(visitor, filename.replace('.py', '')) 69 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_comments/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | TEMP_FOLDER = 'tmp' 4 | MODE_EXECUTABLE = 0o755 5 | MODE_NON_EXECUTABLE = 0o644 6 | 7 | 8 | @pytest.fixture 9 | def make_file(tmp_path): 10 | """Fixture to make a temporary executable or non executable file.""" 11 | 12 | def factory( 13 | filename: str, 14 | file_content: str, 15 | *, 16 | is_executable: bool, 17 | ) -> str: 18 | temp_folder = tmp_path / TEMP_FOLDER 19 | temp_folder.mkdir() 20 | test_file = temp_folder / filename 21 | file_mode = MODE_EXECUTABLE if is_executable else MODE_NON_EXECUTABLE 22 | 23 | test_file.write_text(file_content) 24 | test_file.chmod(file_mode) 25 | 26 | return test_file.as_posix() 27 | 28 | return factory 29 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_comments/test_forbidden_noqa.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | ForbiddenInlineIgnoreViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.tokenize.comments import NoqaVisitor 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('code', 'forbidden_inline_ignore'), 11 | [ 12 | ('x = 10_00 # noqa: WPS002,Z114', ('A', 'C', 'WPS002')), 13 | ('x = 10_00 # noqa:W002, U114', ('W',)), 14 | ('x = 10_00 # noqa: J002, WPS114', ('J', 'WPS')), 15 | ('x = 10_00 # noqa: J, WPS114', ('J',)), 16 | ('x = 10_00 # noqa: WPS114', ('WPS',)), 17 | ], 18 | ) 19 | def test_forbidden_noqa( 20 | parse_tokens, 21 | assert_errors, 22 | options, 23 | code, 24 | forbidden_inline_ignore, 25 | ): 26 | """Ensure that noqa comments with forbidden violations raise a violation.""" 27 | file_tokens = parse_tokens(code) 28 | options = options(forbidden_inline_ignore=forbidden_inline_ignore) 29 | visitor = NoqaVisitor(options, file_tokens=file_tokens) 30 | visitor.run() 31 | assert_errors(visitor, [ForbiddenInlineIgnoreViolation]) 32 | 33 | 34 | @pytest.mark.parametrize( 35 | ('code', 'forbidden_inline_ignore'), 36 | [ 37 | ('x = 10_00 # noqa: WPS002,Z114', ('W',)), 38 | ('x = 10_00 # noqa: WPS002,Z114', ('Z1',)), 39 | ], 40 | ) 41 | def test_correct_noqa( 42 | parse_tokens, 43 | assert_errors, 44 | options, 45 | code, 46 | forbidden_inline_ignore, 47 | ): 48 | """Ensure that proper noqa comments do not rise violations.""" 49 | file_tokens = parse_tokens(code) 50 | options = options(forbidden_inline_ignore=forbidden_inline_ignore) 51 | visitor = NoqaVisitor(options, file_tokens=file_tokens) 52 | visitor.run() 53 | assert_errors(visitor, []) 54 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_comments/test_no_cover_comment.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.constants import MAX_NO_COVER_COMMENTS 4 | from wemake_python_styleguide.violations.best_practices import ( 5 | OveruseOfNoCoverCommentViolation, 6 | ) 7 | from wemake_python_styleguide.visitors.tokenize.comments import ( 8 | WrongCommentVisitor, 9 | ) 10 | 11 | 12 | @pytest.mark.parametrize( 13 | 'code', 14 | [ 15 | 'wallet = 10 # pragma: no cover', 16 | 'wallet = 10 # pragma: no cover', 17 | 'wallet = 10 # pragma: no cover', 18 | 'wallet = 10 # pragma: no cover', 19 | ], 20 | ) 21 | def test_no_cover_overuse( 22 | parse_tokens, 23 | assert_errors, 24 | assert_error_text, 25 | default_options, 26 | code, 27 | ): 28 | """Ensures that `no cover` overuse raises a warning.""" 29 | file_tokens = parse_tokens(f'{code}\n' * (5 + 1)) 30 | 31 | visitor = WrongCommentVisitor(default_options, file_tokens=file_tokens) 32 | visitor.run() 33 | 34 | assert_errors(visitor, [OveruseOfNoCoverCommentViolation]) 35 | assert_error_text(visitor, '6', MAX_NO_COVER_COMMENTS) 36 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_comments/test_noqa_comment.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | WrongMagicCommentViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.tokenize.comments import NoqaVisitor 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 'code', 11 | [ 12 | 'x = 10_00 # noqa: WPS002,Z114', 13 | 'x = 10_00 # noqa:A002, U114', 14 | 'x = 10_00 # noqa: J002, WPS114', 15 | 'wallet = 10_00 # noqa: CPP002', 16 | 'x = 1000 # noqa: DJ002', 17 | 'x = 1000 # noqa: WPS002 ', 18 | 'print(12 + 3) # regular comment', 19 | 'print(12 + 3) #', 20 | 'print(12 + 3)', 21 | '', 22 | ], 23 | ) 24 | def test_correct_comments( 25 | parse_tokens, 26 | assert_errors, 27 | default_options, 28 | code, 29 | ): 30 | """Ensures that correct comments do not raise a warning.""" 31 | file_tokens = parse_tokens(code) 32 | 33 | visitor = NoqaVisitor(default_options, file_tokens=file_tokens) 34 | visitor.run() 35 | 36 | assert_errors(visitor, []) 37 | 38 | 39 | @pytest.mark.parametrize( 40 | 'code', 41 | [ 42 | 'x = 10_00 # noqa WPS002', 43 | 'x = 10_00 # noqa', 44 | 'x = 10_00 # noqa ', 45 | 'x = 10_00 #noqa', 46 | 'x = 10_00#noqa', 47 | 'wallet = 10_00 # noqa: some comments', 48 | 'x = 1000 # noqa:', 49 | 'x = 10_00 # noqa: -', 50 | 'x = 10_00 # noqa: *', 51 | '# noqa', 52 | ], 53 | ) 54 | def test_incorrect_noqa_comment( 55 | parse_tokens, 56 | assert_errors, 57 | default_options, 58 | code, 59 | ): 60 | """Ensures that incorrect `noqa` comments raise a warning.""" 61 | file_tokens = parse_tokens(code) 62 | 63 | visitor = NoqaVisitor(default_options, file_tokens=file_tokens) 64 | visitor.run() 65 | 66 | assert_errors(visitor, [WrongMagicCommentViolation]) 67 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_comments/test_wrong_doc_comment.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | WrongDocCommentViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.tokenize.comments import ( 7 | WrongCommentVisitor, 8 | ) 9 | 10 | constant_doc = """ 11 | #: {0} 12 | SOME_CONSTANT = 12 13 | """ 14 | 15 | attribute_doc = """ 16 | class SomeClass: 17 | #: {0} 18 | some_field = 'text' 19 | """ 20 | 21 | 22 | @pytest.mark.parametrize( 23 | 'code', 24 | [ 25 | constant_doc, 26 | attribute_doc, 27 | ], 28 | ) 29 | @pytest.mark.parametrize( 30 | 'comment', 31 | [ 32 | 'non empty text', 33 | 'text with :', 34 | ], 35 | ) 36 | def test_correct_comments( 37 | parse_tokens, 38 | assert_errors, 39 | default_options, 40 | code, 41 | comment, 42 | ): 43 | """Ensures that correct comments do not raise a warning.""" 44 | file_tokens = parse_tokens(code.format(comment)) 45 | 46 | visitor = WrongCommentVisitor(default_options, file_tokens=file_tokens) 47 | visitor.run() 48 | 49 | assert_errors(visitor, []) 50 | 51 | 52 | @pytest.mark.parametrize( 53 | 'code', 54 | [ 55 | constant_doc, 56 | attribute_doc, 57 | ], 58 | ) 59 | @pytest.mark.parametrize( 60 | 'comment', 61 | [ 62 | '', 63 | ' ', 64 | ' ', 65 | ], 66 | ) 67 | def test_incorrect_doc_comment( 68 | parse_tokens, 69 | assert_errors, 70 | default_options, 71 | code, 72 | comment, 73 | ): 74 | """Ensures that incorrect doc comments raise a warning.""" 75 | file_tokens = parse_tokens(code.format(comment)) 76 | 77 | visitor = WrongCommentVisitor(default_options, file_tokens=file_tokens) 78 | visitor.run() 79 | 80 | assert_errors(visitor, [WrongDocCommentViolation]) 81 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_conditions/test_implict_elif_oneline.py: -------------------------------------------------------------------------------- 1 | from wemake_python_styleguide.violations.refactoring import ( 2 | ImplicitElifViolation, 3 | ) 4 | from wemake_python_styleguide.visitors.tokenize.conditions import IfElseVisitor 5 | 6 | code_that_breaks = """ 7 | number = int(input()) 8 | if number == 1: 9 | print("1") 10 | else: 11 | if number == 2: print("2") 12 | """ 13 | 14 | code_implict_if_else = """ 15 | if numbers: 16 | my_print('first') 17 | else: 18 | if numbers: 19 | my_print('other') 20 | """ 21 | 22 | 23 | def test_if_else_one_line( 24 | assert_errors, 25 | default_options, 26 | parse_tokens, 27 | ): 28 | """Testing to see if ``if`` statement code is on one line.""" 29 | tokens = parse_tokens(code_that_breaks) 30 | 31 | visitor = IfElseVisitor(default_options, tokens) 32 | visitor.run() 33 | 34 | assert_errors(visitor, [ImplicitElifViolation]) 35 | 36 | 37 | def test_if_else_implict( 38 | assert_errors, 39 | default_options, 40 | parse_tokens, 41 | ): 42 | """Testing to make sure implicit if works.""" 43 | tokens = parse_tokens(code_implict_if_else) 44 | 45 | visitor = IfElseVisitor(default_options, tokens) 46 | visitor.run() 47 | 48 | assert_errors(visitor, [ImplicitElifViolation]) 49 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_primitives/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | function_call = 'print({0})' 4 | assignment = 'some_name = {0}' 5 | assignment_with_expression = 'some_sum = {0} + 123' 6 | default_param = 'def function(some={0}): ...' 7 | default_param_with_type = 'def function(some: int = {0}): ...' 8 | statement_with_expression = 'other_var + {0}' 9 | 10 | 11 | @pytest.fixture( 12 | params=[ 13 | function_call, 14 | assignment, 15 | assignment_with_expression, 16 | default_param, 17 | default_param_with_type, 18 | statement_with_expression, 19 | ], 20 | ) 21 | def primitives_usages(request): 22 | """Fixture to return possible cases of promitives use cases.""" 23 | return request.param 24 | 25 | 26 | @pytest.fixture 27 | def regular_number_wrapper(): 28 | """Fixture to return regular numbers without modifications.""" 29 | 30 | def factory(template: str) -> str: 31 | return template 32 | 33 | return factory 34 | 35 | 36 | @pytest.fixture 37 | def negative_number_wrapper(): 38 | """Fixture to return negative numbers.""" 39 | 40 | def factory(template: str) -> str: 41 | return f'-{template}' 42 | 43 | return factory 44 | 45 | 46 | @pytest.fixture 47 | def positive_number_wrapper(): 48 | """Fixture to return positive numbers with explicit ``+``.""" 49 | 50 | def factory(template: str) -> str: 51 | return f'+{template}' 52 | 53 | return factory 54 | 55 | 56 | @pytest.fixture( 57 | params=[ 58 | 'regular_number_wrapper', 59 | 'negative_number_wrapper', 60 | 'positive_number_wrapper', 61 | ], 62 | ) 63 | def number_sign(request): 64 | """Fixture that returns regular, negative, and positive numbers.""" 65 | return request.getfixturevalue(request.param) 66 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_primitives/test_numbers/test_number_float_zero.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.consistency import FloatZeroViolation 4 | from wemake_python_styleguide.visitors.tokenize.primitives import ( 5 | WrongNumberTokenVisitor, 6 | ) 7 | 8 | 9 | def test_float_zero( 10 | parse_tokens, 11 | assert_errors, 12 | default_options, 13 | primitives_usages, 14 | mode, 15 | ): 16 | """Ensures that float zeros (0.0) raise a warning.""" 17 | file_tokens = parse_tokens(mode(primitives_usages.format('0.0'))) 18 | 19 | visitor = WrongNumberTokenVisitor(default_options, file_tokens=file_tokens) 20 | visitor.run() 21 | 22 | assert_errors(visitor, [FloatZeroViolation]) 23 | 24 | 25 | @pytest.mark.parametrize( 26 | 'primitive', 27 | [ 28 | '0', 29 | 'float(0)', 30 | '5', 31 | '30.4', 32 | ], 33 | ) 34 | def test_correct_zero_and_non_zero_numbers( 35 | parse_tokens, 36 | assert_errors, 37 | default_options, 38 | primitives_usages, 39 | primitive, 40 | mode, 41 | ): 42 | """Ensures that correct zeros and non-zero numbers don't raise a warning.""" 43 | file_tokens = parse_tokens(mode(primitives_usages.format(primitive))) 44 | 45 | visitor = WrongNumberTokenVisitor(default_options, file_tokens=file_tokens) 46 | visitor.run() 47 | 48 | assert_errors(visitor, []) 49 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_primitives/test_string_tokens/test_unicode_escape.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.violations.best_practices import ( 4 | WrongUnicodeEscapeViolation, 5 | ) 6 | from wemake_python_styleguide.visitors.tokenize.primitives import ( 7 | WrongStringTokenVisitor, 8 | ) 9 | 10 | 11 | @pytest.mark.filterwarnings('ignore::SyntaxWarning') 12 | @pytest.mark.parametrize( 13 | 'code', 14 | [ 15 | r"b'\ua'", 16 | r"b'\u1'", 17 | r"b'\Ua'", 18 | r"b'\N{GREEK SMALL LETTER ALPHA}'", 19 | ], 20 | ) 21 | def test_wrong_unicode_escape( # pragma: >=3.12 cover 22 | parse_tokens, 23 | assert_errors, 24 | default_options, 25 | code, 26 | ): 27 | """Ensures that wrong unicode escape raises a warning.""" 28 | try: 29 | file_tokens = parse_tokens(code) 30 | except SyntaxError: # pragma: no cover 31 | pytest.skip(f'SyntaxError on unicode escapes: {code}') 32 | 33 | visitor = WrongStringTokenVisitor(default_options, file_tokens=file_tokens) 34 | visitor.run() 35 | 36 | assert_errors(visitor, [WrongUnicodeEscapeViolation]) 37 | 38 | 39 | @pytest.mark.parametrize( 40 | 'code', 41 | [ 42 | r"'\ua'", 43 | r"'\u1'", 44 | r"'\Ua'", 45 | r"'\N{GREEK SMALL LETTER ALPHA}'", 46 | ], 47 | ) 48 | def test_correct_unicode_escape( 49 | parse_tokens, 50 | assert_errors, 51 | default_options, 52 | code, 53 | ): 54 | """Ensures that correct unicode escape does not raise a warning.""" 55 | file_tokens = parse_tokens(code, do_compile=False) 56 | 57 | visitor = WrongStringTokenVisitor(default_options, file_tokens=file_tokens) 58 | visitor.run() 59 | 60 | assert_errors(visitor, []) 61 | -------------------------------------------------------------------------------- /tests/test_visitors/test_tokenize/test_primitives/test_string_tokens/test_unicode_prefix.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wemake_python_styleguide.visitors.tokenize.primitives import ( 4 | WrongStringTokenVisitor, 5 | ) 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 'primitive', 10 | [ 11 | '"name"', 12 | r'r"text with escape carac \n"', 13 | "b'unicode'", 14 | '"u"', 15 | '"12"', 16 | 'b""', 17 | ], 18 | ) 19 | def test_correct_strings( 20 | parse_tokens, 21 | assert_errors, 22 | default_options, 23 | primitives_usages, 24 | primitive, 25 | mode, 26 | ): 27 | """Ensures that correct strings are fine.""" 28 | file_tokens = parse_tokens( 29 | mode(primitives_usages.format(primitive)), 30 | do_compile=False, 31 | ) 32 | 33 | visitor = WrongStringTokenVisitor(default_options, file_tokens=file_tokens) 34 | visitor.run() 35 | 36 | assert_errors(visitor, []) 37 | -------------------------------------------------------------------------------- /wemake_python_styleguide/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/cli/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/cli_app.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand 4 | 5 | 6 | def _configure_arg_parser() -> argparse.ArgumentParser: 7 | """Configures CLI arguments and subcommands.""" 8 | parser = argparse.ArgumentParser( 9 | prog='wps', description='WPS command line tool' 10 | ) 11 | sub_parsers = parser.add_subparsers( 12 | help='sub-parser for exact wps commands', 13 | required=True, 14 | ) 15 | 16 | parser_explain = sub_parsers.add_parser( 17 | 'explain', 18 | help='Get violation description', 19 | ) 20 | parser_explain.add_argument( 21 | 'violation_code', 22 | help='Desired violation code', 23 | ) 24 | parser_explain.set_defaults(func=ExplainCommand()) 25 | 26 | return parser 27 | 28 | 29 | def parse_args() -> argparse.Namespace: 30 | """Parse CLI arguments.""" 31 | parser = _configure_arg_parser() 32 | return parser.parse_args() 33 | 34 | 35 | def main() -> int: 36 | """Main function.""" 37 | args = parse_args() 38 | return int(args.func(args=args)) 39 | -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/cli/commands/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/commands/base.py: -------------------------------------------------------------------------------- 1 | """Contains files common for all wps commands.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from argparse import Namespace 5 | from typing import Generic, TypeVar 6 | 7 | _ArgsT = TypeVar('_ArgsT') 8 | 9 | 10 | class AbstractCommand(ABC, Generic[_ArgsT]): 11 | """ABC for all commands.""" 12 | 13 | _args_type: type[_ArgsT] 14 | 15 | def __call__(self, args: Namespace) -> int: 16 | """Parse arguments into the generic namespace.""" 17 | args_dict = vars(args) # noqa: WPS421 18 | args_dict.pop('func') # argument classes do not expect that 19 | cmd_args = self._args_type(**args_dict) 20 | return self._run(cmd_args) 21 | 22 | @abstractmethod 23 | def _run(self, args: _ArgsT) -> int: 24 | """Run the command.""" 25 | raise NotImplementedError 26 | -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/commands/explain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/cli/commands/explain/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/commands/explain/command.py: -------------------------------------------------------------------------------- 1 | """Contains command implementation.""" 2 | 3 | from typing import final 4 | 5 | from attrs import frozen 6 | 7 | from wemake_python_styleguide.cli.commands.base import AbstractCommand 8 | from wemake_python_styleguide.cli.commands.explain import ( 9 | message_formatter, 10 | violation_loader, 11 | ) 12 | from wemake_python_styleguide.cli.output import print_stderr, print_stdout 13 | 14 | 15 | def _clean_violation_code(violation_str: str) -> int: 16 | """Get int violation code from str violation code.""" 17 | violation_str = violation_str.removeprefix('WPS') 18 | try: 19 | return int(violation_str) 20 | except ValueError: 21 | return -1 22 | 23 | 24 | @final 25 | @frozen 26 | class ExplainCommandArgs: 27 | """Arguments for wps explain command.""" 28 | 29 | violation_code: str 30 | 31 | 32 | @final 33 | class ExplainCommand(AbstractCommand[ExplainCommandArgs]): 34 | """Explain command impl.""" 35 | 36 | _args_type = ExplainCommandArgs 37 | 38 | def _run(self, args: ExplainCommandArgs) -> int: 39 | """Run command.""" 40 | code = _clean_violation_code(args.violation_code) 41 | violation = violation_loader.get_violation(code) 42 | if violation is None: 43 | print_stderr(f'Violation "{args.violation_code}" not found') 44 | return 1 45 | message = message_formatter.format_violation(violation) 46 | print_stdout(message) 47 | return 0 48 | -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/commands/explain/message_formatter.py: -------------------------------------------------------------------------------- 1 | """Provides tools for formatting explanations.""" 2 | 3 | import textwrap 4 | 5 | from wemake_python_styleguide.cli.commands.explain.violation_loader import ( 6 | ViolationInfo, 7 | ) 8 | from wemake_python_styleguide.constants import SHORTLINK_TEMPLATE 9 | 10 | 11 | def _remove_newlines_at_ends(text: str) -> str: 12 | """Remove leading and trailing newlines.""" 13 | return text.strip('\n\r') 14 | 15 | 16 | def format_violation(violation: ViolationInfo) -> str: 17 | """Format violation information.""" 18 | cleaned_docstring = _remove_newlines_at_ends( 19 | textwrap.dedent(violation.docstring) 20 | ) 21 | violation_url = SHORTLINK_TEMPLATE.format(f'WPS{violation.code}') 22 | return f'{cleaned_docstring}\n\nSee at website: {violation_url}' 23 | -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/commands/explain/module_loader.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from collections.abc import Collection 3 | from pathlib import Path 4 | from types import ModuleType 5 | from typing import Final 6 | 7 | _VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' 8 | 9 | 10 | def get_violation_submodules() -> Collection[ModuleType]: 11 | """Get all possible violation submodules.""" 12 | submodule_names = _get_all_possible_submodule_names(_VIOLATION_MODULE_BASE) 13 | return [ 14 | importlib.import_module(submodule_name) 15 | for submodule_name in submodule_names 16 | ] 17 | 18 | 19 | def _get_all_possible_submodule_names(module_name: str) -> Collection[str]: 20 | """Get .py submodule names listed in given module.""" 21 | root_module = importlib.import_module(module_name) 22 | root_paths = root_module.__path__ 23 | names = [] 24 | for root in root_paths: 25 | names.extend([ 26 | f'{module_name}.{name}' 27 | for name in _get_all_possible_names_in_root(root) 28 | ]) 29 | return names 30 | 31 | 32 | def _get_all_possible_names_in_root(root: str) -> Collection[str]: 33 | """Get .py submodule names listed in given root path.""" 34 | return [ 35 | path.name.removesuffix('.py') 36 | for path in Path(root).glob('*.py') 37 | if '__' not in path.name # filter dunder files like __init__.py 38 | ] 39 | -------------------------------------------------------------------------------- /wemake_python_styleguide/cli/output.py: -------------------------------------------------------------------------------- 1 | """Provides tool for outputting data.""" 2 | 3 | import sys 4 | 5 | 6 | def print_stdout(*args: str) -> None: 7 | """Write usual text. Works as print.""" 8 | sys.stdout.write(' '.join(args)) 9 | sys.stdout.write('\n') 10 | sys.stdout.flush() 11 | 12 | 13 | def print_stderr(*args: str) -> None: 14 | """Write error text. Works as print.""" 15 | sys.stderr.write(' '.join(args)) 16 | sys.stderr.write('\n') 17 | sys.stderr.flush() 18 | -------------------------------------------------------------------------------- /wemake_python_styleguide/compat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/compat/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/compat/aliases.py: -------------------------------------------------------------------------------- 1 | """ 2 | Here we store useful aliases to make sure code works between versions. 3 | 4 | Please, document everything you do. 5 | Add links to the changes in the changelog if possible. 6 | And provide links to the python source code. 7 | """ 8 | 9 | import ast 10 | from typing import Final, final 11 | 12 | 13 | @final 14 | class _TextNodesMeta(type): 15 | def __instancecheck__(cls, instance): 16 | return isinstance(instance, ast.Constant) and isinstance( 17 | instance.value, 18 | str | bytes, 19 | ) 20 | 21 | 22 | @final 23 | class TextNodes(ast.AST, metaclass=_TextNodesMeta): 24 | """Check if node has type of `ast.Constant` with `str` or `bytes`.""" 25 | 26 | value: str | bytes # noqa: WPS110 27 | 28 | 29 | #: We need this tuple to easily check that this is a real assign node. 30 | AssignNodes: Final = (ast.Assign, ast.AnnAssign) 31 | 32 | #: We need this tuple for cases where we use full assign power. 33 | AssignNodesWithWalrus: Final = (*AssignNodes, ast.NamedExpr) 34 | 35 | #: We need this tuple since ``async def`` now has its own ast class. 36 | FunctionNodes: Final = (ast.FunctionDef, ast.AsyncFunctionDef) 37 | 38 | #: We need this tuple since ``ast.AsyncFor``` was introduced. 39 | ForNodes: Final = (ast.For, ast.AsyncFor) 40 | 41 | #: We need this tuple since ``ast.AsyncWith`` was introduced. 42 | WithNodes: Final = (ast.With, ast.AsyncWith) 43 | -------------------------------------------------------------------------------- /wemake_python_styleguide/compat/constants.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Final 3 | 4 | #: This indicates that we are running on python3.11+ 5 | PY311: Final = sys.version_info >= (3, 11) 6 | 7 | # This indicates that we are running on python3.12+ 8 | PY312: Final = sys.version_info >= (3, 12) 9 | 10 | # This indicates that we are running on python3.13+ 11 | PY313: Final = sys.version_info >= (3, 13) 12 | -------------------------------------------------------------------------------- /wemake_python_styleguide/compat/functions.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.compat.types import NodeWithTypeParams 4 | from wemake_python_styleguide.types import AnyAssignWithWalrus 5 | 6 | 7 | def get_assign_targets( 8 | node: AnyAssignWithWalrus | ast.AugAssign, 9 | ) -> list[ast.expr]: 10 | """Returns list of assign targets without knowing the type of assign.""" 11 | if isinstance(node, ast.AnnAssign | ast.AugAssign | ast.NamedExpr): 12 | return [node.target] 13 | return node.targets 14 | 15 | 16 | def get_type_param_names( # pragma: >=3.12 cover 17 | node: NodeWithTypeParams, 18 | ) -> list[tuple[ast.AST, str]]: 19 | """Return list of type parameters' names.""" 20 | type_params = [] 21 | for type_param_node in getattr(node, 'type_params', []): 22 | type_param_name = type_param_node.name 23 | type_params.append((type_param_node, type_param_name)) 24 | return type_params 25 | -------------------------------------------------------------------------------- /wemake_python_styleguide/compat/nodes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to define fix type differences in different python versions. 3 | 4 | Note that we use ``sys.version_info`` directly, 5 | because that's how ``mypy`` knows about what we are doing. 6 | """ 7 | 8 | import ast 9 | import sys 10 | 11 | if sys.version_info >= (3, 11): # pragma: >=3.11 cover 12 | from ast import TryStar as TryStar 13 | else: # pragma: <3.11 cover 14 | 15 | class TryStar(ast.stmt): 16 | """Used for `try/except*` statements.""" 17 | 18 | body: list[ast.stmt] 19 | handlers: list[ast.ExceptHandler] 20 | orelse: list[ast.stmt] 21 | finalbody: list[ast.stmt] 22 | 23 | 24 | if sys.version_info >= (3, 12): # pragma: >=3.12 cover 25 | from ast import TypeAlias as TypeAlias 26 | else: # pragma: <3.12 cover 27 | 28 | class TypeAlias(ast.stmt): 29 | """Used to define `TypeAlias` nodes in `python3.12+`.""" 30 | 31 | name: ast.Name 32 | type_params: list[ast.stmt] 33 | value: ast.expr # noqa: WPS110 34 | 35 | 36 | if sys.version_info >= (3, 13): # pragma: >=3.13 cover 37 | from ast import TypeVar as TypeVar 38 | from ast import TypeVarTuple as TypeVarTuple 39 | else: # pragma: <3.13 cover 40 | 41 | class TypeVar(ast.AST): 42 | """Used to define `TypeVar` nodes from `python3.12+`.""" 43 | 44 | name: str 45 | bound: ast.expr | None # noqa: WPS110 46 | default_value: ast.AST | None 47 | 48 | class TypeVarTuple(ast.AST): 49 | """Used to define `TypeVarTuple` nodes from `python3.12+`.""" 50 | 51 | name: str 52 | -------------------------------------------------------------------------------- /wemake_python_styleguide/compat/packaging.py: -------------------------------------------------------------------------------- 1 | from importlib import metadata as importlib_metadata 2 | 3 | 4 | def get_version(distribution_name: str) -> str: 5 | """Our helper to get version of a package.""" 6 | return importlib_metadata.version(distribution_name) 7 | -------------------------------------------------------------------------------- /wemake_python_styleguide/compat/routing.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import types 3 | from typing import Final 4 | 5 | #: That's how python types and ast types map to each other, copied from ast. 6 | _CONST_NODE_TYPE_NAMES: Final = types.MappingProxyType( 7 | { 8 | bool: 'NameConstant', # should be before int 9 | type(None): 'NameConstant', 10 | int: 'Num', 11 | float: 'Num', 12 | complex: 'Num', 13 | str: 'Str', 14 | bytes: 'Bytes', 15 | type(...): 'Ellipsis', 16 | }, 17 | ) 18 | 19 | 20 | def route_visit(self: ast.NodeVisitor, node: ast.AST) -> None: 21 | """ 22 | Custom router for python3.8+ release. 23 | 24 | Hacked to make sure that everything we had defined before is working. 25 | """ 26 | if isinstance(node, ast.Constant): 27 | # That's the hack itself, we don't get the name of the node. 28 | # We get the name of wrapped type from it. 29 | type_name = _CONST_NODE_TYPE_NAMES.get(type(node.value)) 30 | else: 31 | type_name = node.__class__.__name__ 32 | 33 | return getattr( # type: ignore[no-any-return] 34 | self, 35 | f'visit_{type_name}', 36 | self.generic_visit, 37 | )(node) 38 | -------------------------------------------------------------------------------- /wemake_python_styleguide/compat/types.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import TypeAlias 3 | 4 | from wemake_python_styleguide.compat.nodes import TryStar 5 | from wemake_python_styleguide.compat.nodes import TypeAlias as TypeAliasNode 6 | 7 | #: When used with `visit_Try` and visit_TryStar`. 8 | AnyTry: TypeAlias = ast.Try | TryStar 9 | 10 | #: Used when named matches are needed. 11 | NamedMatch: TypeAlias = ast.MatchAs | ast.MatchStar 12 | 13 | #: These nodes have `.type_params` on python3.12+: 14 | NodeWithTypeParams: TypeAlias = ( 15 | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef | TypeAliasNode 16 | ) 17 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/logic/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/arguments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/logic/arguments/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/arguments/call_args.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from collections.abc import Iterator, Sequence 3 | 4 | 5 | def get_all_args(call: ast.Call) -> Sequence[ast.AST]: 6 | """Gets all arguments (args and kwargs) from ``ast.Call``.""" 7 | return [ 8 | *call.args, 9 | *[kw.value for kw in call.keywords], 10 | ] 11 | 12 | 13 | def get_starred_args(call: ast.Call) -> Iterator[ast.Starred]: 14 | """Gets ``ast.Starred`` arguments from ``ast.Call``.""" 15 | for argument in call.args: 16 | if isinstance(argument, ast.Starred): 17 | yield argument 18 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/arguments/special_args.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide import constants, types 4 | 5 | 6 | def clean_special_argument( 7 | node: types.AnyFunctionDefAndLambda, 8 | node_args: list[ast.arg], 9 | ) -> list[ast.arg]: 10 | """Removes ``self``, ``cls``, ``mcs`` from argument lists.""" 11 | if not node_args or isinstance(node, ast.Lambda): 12 | return node_args 13 | if node_args[0].arg not in constants.SPECIAL_ARGUMENT_NAMES_WHITELIST: 14 | return node_args 15 | return node_args[1:] 16 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/complexity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/logic/complexity/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/complexity/functions.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import TypeAlias, final 3 | 4 | import attr 5 | 6 | from wemake_python_styleguide.types import ( 7 | AnyFunctionDef, 8 | AnyFunctionDefAndLambda, 9 | ) 10 | 11 | #: Function complexity counter. 12 | FunctionCounter: TypeAlias = defaultdict[AnyFunctionDef, int] 13 | 14 | #: Function and lambda complexity counter. 15 | FunctionCounterWithLambda: TypeAlias = defaultdict[AnyFunctionDefAndLambda, int] 16 | 17 | #: Function and their variables. 18 | FunctionNames: TypeAlias = defaultdict[AnyFunctionDef, list[str]] 19 | 20 | 21 | def _default_factory() -> FunctionCounter: 22 | """We use a lot of defaultdic magic in these metrics.""" 23 | return defaultdict(int) 24 | 25 | 26 | @final 27 | @attr.dataclass(frozen=False) 28 | class ComplexityMetrics: 29 | """ 30 | Complexity metrics for functions. 31 | 32 | We use it as a store of all metrics we count in a function's body. 33 | There are quite a lot of them! 34 | """ 35 | 36 | returns: FunctionCounter = attr.ib(factory=_default_factory) 37 | raises: FunctionCounter = attr.ib(factory=_default_factory) 38 | awaits: FunctionCounter = attr.ib(factory=_default_factory) 39 | asserts: FunctionCounter = attr.ib(factory=_default_factory) 40 | expressions: FunctionCounter = attr.ib(factory=_default_factory) 41 | arguments: FunctionCounterWithLambda = attr.ib(factory=_default_factory) 42 | variables: FunctionNames = attr.ib(factory=lambda: defaultdict(list)) 43 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/filenames.py: -------------------------------------------------------------------------------- 1 | from pathlib import PurePath 2 | 3 | 4 | def get_stem(file_path: str) -> str: 5 | """ 6 | Returns the last element of path without extension. 7 | 8 | >>> get_stem('/some/module.py') 9 | 'module' 10 | 11 | >>> get_stem('C:/User/package/__init__.py') 12 | '__init__' 13 | 14 | >>> get_stem('c:/package/abc.py') 15 | 'abc' 16 | 17 | >>> get_stem('episode2.py') 18 | 'episode2' 19 | 20 | """ 21 | return PurePath(file_path).stem 22 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/naming/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/logic/naming/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/naming/blacklists.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | 3 | from wemake_python_styleguide.constants import ( 4 | MODULE_METADATA_VARIABLES_BLACKLIST, 5 | VARIABLE_NAMES_BLACKLIST, 6 | ) 7 | from wemake_python_styleguide.options.validation import ValidatedOptions 8 | 9 | 10 | @cache 11 | def variable_names_blacklist_from( 12 | options: ValidatedOptions, 13 | ) -> frozenset[str]: 14 | """Creates variable names blacklist from options and constants.""" 15 | variable_names_blacklist = { 16 | *VARIABLE_NAMES_BLACKLIST, 17 | *options.forbidden_domain_names, 18 | } 19 | return frozenset( 20 | variable_names_blacklist - set(options.allowed_domain_names), 21 | ) 22 | 23 | 24 | @cache 25 | def module_metadata_blacklist( 26 | options: ValidatedOptions, 27 | ) -> frozenset[str]: 28 | """Creates module metadata blacklist from options and constants.""" 29 | module_metadata_blacklist = { 30 | *MODULE_METADATA_VARIABLES_BLACKLIST, 31 | *options.forbidden_module_metadata, 32 | } 33 | return frozenset( 34 | module_metadata_blacklist - set(options.allowed_module_metadata), 35 | ) 36 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/naming/builtins.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | import inspect 3 | import keyword 4 | from typing import Final 5 | 6 | from wemake_python_styleguide.logic.naming.access import is_magic, is_unused 7 | 8 | _BUILTINS: Final = frozenset( 9 | builtin[0] for builtin in inspect.getmembers(builtins) 10 | ) 11 | 12 | _ALL_BUILTINS: Final = frozenset( 13 | ( 14 | *keyword.kwlist, 15 | *_BUILTINS, 16 | # Special case. 17 | # Some python version have them, some do not have them: 18 | 'async', 19 | 'await', 20 | ), 21 | ) 22 | 23 | 24 | def is_builtin_name(variable_name: str) -> bool: 25 | """ 26 | Tells whether a variable name is builtin or not. 27 | 28 | >>> is_builtin_name('str') 29 | True 30 | 31 | >>> is_builtin_name('_') 32 | False 33 | 34 | >>> is_builtin_name('custom') 35 | False 36 | 37 | >>> is_builtin_name('Exception') 38 | True 39 | 40 | >>> is_builtin_name('async') 41 | True 42 | 43 | """ 44 | return variable_name in _ALL_BUILTINS 45 | 46 | 47 | def is_wrong_alias(variable_name: str) -> bool: 48 | """ 49 | Tells whether a variable is wrong builtins alias or not. 50 | 51 | >>> is_wrong_alias('regular_name_') 52 | True 53 | 54 | >>> is_wrong_alias('_') 55 | False 56 | 57 | >>> is_wrong_alias('_async') 58 | False 59 | 60 | >>> is_wrong_alias('_await') 61 | False 62 | 63 | >>> is_wrong_alias('regular_name') 64 | False 65 | 66 | >>> is_wrong_alias('class_') 67 | False 68 | 69 | >>> is_wrong_alias('list_') 70 | False 71 | 72 | >>> is_wrong_alias('list') 73 | False 74 | 75 | >>> is_wrong_alias('__spec__') 76 | False 77 | 78 | """ 79 | if is_magic(variable_name): 80 | return False 81 | 82 | if is_unused(variable_name) or not variable_name.endswith('_'): 83 | return False 84 | 85 | return not is_builtin_name(variable_name[:-1]) 86 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/naming/constants.py: -------------------------------------------------------------------------------- 1 | from wemake_python_styleguide.logic.naming.access import is_unused 2 | 3 | 4 | def is_constant(name: str) -> bool: 5 | """ 6 | Checks whether the given ``name`` is a constant. 7 | 8 | >>> is_constant('CONST') 9 | True 10 | 11 | >>> is_constant('ALLOWED_EMPTY_LINE_TOKEN') 12 | True 13 | 14 | >>> is_constant('Some') 15 | False 16 | 17 | >>> is_constant('_') 18 | False 19 | 20 | >>> is_constant('lower_case') 21 | False 22 | 23 | """ 24 | if is_unused(name): 25 | return False 26 | 27 | return all( 28 | # We check that constant names consist of: 29 | # UPPERCASE LETTERS and `_` char 30 | character.isupper() or character == '_' 31 | for character in name 32 | ) 33 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/naming/duplicates.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | 4 | def get_duplicate_names(variables: list[set[str]]) -> set[str]: 5 | """ 6 | Find duplicate names in different nodes. 7 | 8 | >>> get_duplicate_names([{'a', 'b'}, {'b', 'c'}]) 9 | {'b'} 10 | """ 11 | return reduce( 12 | lambda acc, element: acc.intersection(element), 13 | variables, 14 | ) 15 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/nodes.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.types import ContextNodes 4 | 5 | 6 | def is_literal(node: ast.AST) -> bool: 7 | """ 8 | Checks for nodes that contains only constants. 9 | 10 | If the node contains only literals it will be evaluated. 11 | When node relies on some other names, it won't be evaluated. 12 | """ 13 | try: 14 | ast.literal_eval(node) 15 | except ValueError: 16 | return False 17 | return True 18 | 19 | 20 | def get_parent(node: ast.AST) -> ast.AST | None: 21 | """Returns the parent node or ``None`` if node has no parent.""" 22 | return getattr(node, 'wps_parent', None) 23 | 24 | 25 | def get_context(node: ast.AST) -> ContextNodes | None: 26 | """Returns the context or ``None`` if node has no context.""" 27 | return getattr(node, 'wps_context', None) 28 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/source.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.types import AnyTextPrimitive 4 | 5 | 6 | def node_to_string(node: ast.AST) -> str: 7 | """Returns the source code by doing ``ast`` to string convert.""" 8 | return ast.unparse(node).strip() 9 | 10 | 11 | def render_string(text_data: AnyTextPrimitive) -> str: 12 | """ 13 | Method to render ``Str``, ``Bytes``, and f-string nodes to ``str``. 14 | 15 | Keep in mind, that bytes with wrong chars will be rendered incorrectly 16 | But, this is not important for the business logic. 17 | 18 | """ 19 | if isinstance(text_data, bytes): 20 | # See https://docs.python.org/3/howto/unicode.html 21 | return text_data.decode('utf-8', errors='surrogateescape') 22 | return text_data # it is a `str` 23 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/system.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from wemake_python_styleguide.constants import WINDOWS_OS 4 | 5 | 6 | def is_executable_file(filename: str) -> bool: 7 | """Checks if a file is executable.""" 8 | return os.access(filename, os.X_OK) 9 | 10 | 11 | def is_windows() -> bool: 12 | """Checks if we are running on Windows.""" 13 | return os.name == WINDOWS_OS 14 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tokens/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/logic/tokens/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tokens/constants.py: -------------------------------------------------------------------------------- 1 | import tokenize 2 | from typing import Final 3 | 4 | #: Constant for several types of new lines in Python's grammar. 5 | NEWLINES: Final = frozenset( 6 | ( 7 | tokenize.NL, 8 | tokenize.NEWLINE, 9 | ), 10 | ) 11 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tokens/newlines.py: -------------------------------------------------------------------------------- 1 | import tokenize 2 | from collections.abc import Sequence 3 | 4 | from wemake_python_styleguide.logic.tokens.constants import NEWLINES 5 | 6 | 7 | def next_meaningful_token( 8 | tokens: Sequence[tokenize.TokenInfo], 9 | token_position: int, 10 | ) -> tokenize.TokenInfo: 11 | """ 12 | Returns the next meaningful (non-newline) token. 13 | 14 | Please, make sure that `tokens` are non empty. 15 | We don't want to make this return `Optional`. 16 | 17 | Get ready for some `IndexError` if `tokens` is messed up. 18 | """ 19 | # This looks like a bug in coverage.py! 20 | # Because we test all the possibilities here. 21 | return next( # pragma: no cover 22 | tokens[index] 23 | for index in range(token_position + 1, len(tokens)) 24 | if tokens[index].exact_type not in NEWLINES 25 | ) 26 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tokens/numbers.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Final 3 | 4 | _UNDERSCORE_PATTERN: Final = re.compile(r'^\d{1,3}(_\d{3})*$') 5 | _SPLIT_PATTERN: Final = re.compile(r'\.|e[\+-]?') 6 | 7 | 8 | def has_correct_underscores(number: str) -> bool: 9 | """ 10 | Formats a number as a string separated by thousands with support floating. 11 | 12 | >>> has_correct_underscores('1_234.157_000e-1_123') 13 | True 14 | 15 | >>> has_correct_underscores('0b1_001') 16 | True 17 | 18 | >>> has_correct_underscores('12_345.987_654_321') 19 | True 20 | 21 | >>> has_correct_underscores('10000_000_00') 22 | False 23 | """ 24 | assert '_' in number # noqa: S101 25 | number_cleared = ( 26 | number.strip() 27 | .lower() 28 | .removeprefix('0b') 29 | .removeprefix('0x') 30 | .removeprefix('0o') 31 | .removesuffix('j') 32 | ) 33 | return all( 34 | _UNDERSCORE_PATTERN.match(number_part) 35 | for number_part in _SPLIT_PATTERN.split(number_cleared) 36 | ) 37 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tokens/strings.py: -------------------------------------------------------------------------------- 1 | import tokenize 2 | from typing import Final 3 | 4 | #: All tokens that don't really mean anything for user. 5 | _UTILITY_TOKENS: Final = frozenset(( 6 | tokenize.NEWLINE, 7 | tokenize.INDENT, 8 | tokenize.DEDENT, 9 | tokenize.NL, 10 | tokenize.COMMENT, 11 | )) 12 | 13 | 14 | def split_prefixes(string: str) -> tuple[str, str]: 15 | """ 16 | Splits string repr by prefixes and the quoted content. 17 | 18 | Returns the tuple of modifiers and untouched internal string contents. 19 | 20 | >>> split_prefixes("Br'test'") 21 | ('Br', "'test'") 22 | 23 | >>> split_prefixes("'test'") 24 | ('', "'test'") 25 | 26 | """ 27 | split = string.split(string[-1]) 28 | return split[0], string.replace(split[0], '', 1) 29 | 30 | 31 | def has_triple_string_quotes(string_contents: str) -> bool: 32 | """Tells whether string token is written as inside triple quotes.""" 33 | _mods, string_contents = split_prefixes(string_contents) 34 | return bool( 35 | (string_contents.startswith('"""') and string_contents.endswith('"""')) 36 | or ( 37 | string_contents.startswith("'''") 38 | and string_contents.endswith("'''") 39 | ), 40 | ) 41 | 42 | 43 | def get_comment_text(token: tokenize.TokenInfo) -> str: 44 | """Returns comment without `#` char from comment tokens.""" 45 | return token.string[1:].strip() 46 | 47 | 48 | def is_meaningful_token(token: tokenize.TokenInfo) -> bool: 49 | """Returns `True` if some token is a real, not utility token.""" 50 | return token.exact_type not in _UTILITY_TOKENS 51 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/logic/tree/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/annotations.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import Final 3 | 4 | from wemake_python_styleguide.compat.aliases import FunctionNodes 5 | from wemake_python_styleguide.logic import walk 6 | 7 | #: Nodes that can be directly annotated. 8 | _AnnNodes: Final = (ast.AnnAssign, ast.arg) 9 | 10 | #: Nodes that can be a part of an annotation. 11 | _AnnParts: Final = ( 12 | ast.Name, 13 | ast.Attribute, 14 | ast.List, 15 | ast.Tuple, 16 | ast.Subscript, 17 | ast.BinOp, # new styled unions, like: `str | int` 18 | ) 19 | 20 | 21 | def is_annotation(node: ast.AST) -> bool: 22 | """ 23 | Detects if node is an annotation. Or a part of it. 24 | 25 | We use this predicate to allow all types of repetitive 26 | function and instance annotations. 27 | """ 28 | if not ( 29 | isinstance(node, _AnnParts) 30 | or (isinstance(node, ast.Constant) and isinstance(node.value, str)) 31 | ): 32 | return False 33 | 34 | annotated = walk.get_closest_parent(node, (*_AnnNodes, *FunctionNodes)) 35 | if isinstance(annotated, FunctionNodes): 36 | contains_node = bool( 37 | annotated.returns and walk.is_contained_by(node, annotated.returns), 38 | ) 39 | return node == annotated.returns or contains_node 40 | if isinstance(annotated, _AnnNodes): 41 | contains_node = bool( 42 | annotated.annotation 43 | and walk.is_contained_by(node, annotated.annotation), 44 | ) 45 | return node == annotated.annotation or contains_node 46 | return False 47 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/bools.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | 4 | def count_boolops(node: ast.AST) -> int: 5 | """Counts how many ``BoolOp`` nodes there are in a node.""" 6 | return len( 7 | [ 8 | subnode 9 | for subnode in ast.walk(node) 10 | if isinstance(subnode, ast.BoolOp) 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/calls.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from collections.abc import Iterable 3 | 4 | 5 | def _chained_item(iterator: ast.AST) -> ast.Call | None: 6 | children = list(ast.iter_child_nodes(iterator)) 7 | if isinstance(children[0], ast.Call): 8 | return children[0] 9 | return None 10 | 11 | 12 | def parts(node: ast.Call) -> Iterable[ast.Call]: 13 | """Returns all consecutive function calls.""" 14 | iterator: ast.Call = node 15 | 16 | while True: 17 | yield iterator 18 | 19 | chained_item = _chained_item(iterator) 20 | if chained_item is None: 21 | return 22 | iterator = chained_item 23 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/collections.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from collections.abc import Iterable, Sequence 3 | from functools import partial 4 | from typing import TypeVar, cast 5 | 6 | _NodeType = TypeVar('_NodeType') 7 | _DefaultType = TypeVar('_DefaultType') 8 | 9 | 10 | def sequence_of_node( 11 | node_types: tuple[type[_NodeType], ...], 12 | sequence: Sequence[ast.stmt], 13 | ) -> Iterable[Sequence[_NodeType]]: 14 | """Find sequence of node by type.""" 15 | is_desired_type = partial( 16 | lambda types, node: isinstance(node, types), 17 | node_types, 18 | ) 19 | 20 | sequence_iterator = iter(sequence) 21 | previous_node = next(sequence_iterator, None) 22 | node_sequence: list[_NodeType] = [] 23 | 24 | while previous_node is not None: 25 | current_node = next(sequence_iterator, None) 26 | 27 | if all(map(is_desired_type, (previous_node, current_node))): 28 | node_sequence.append(cast(_NodeType, previous_node)) 29 | elif node_sequence: 30 | yield [*node_sequence, cast(_NodeType, previous_node)] 31 | node_sequence = [] 32 | 33 | previous_node = current_node 34 | 35 | 36 | def first( 37 | sequence: Iterable[_NodeType], 38 | default: _DefaultType | None = None, 39 | ) -> _NodeType | _DefaultType | None: 40 | """Get first variable from sequence or default.""" 41 | return next(iter(sequence), default) 42 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/decorators.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.types import AnyFunctionDef 4 | 5 | 6 | def has_overload_decorator(function: AnyFunctionDef) -> bool: 7 | """ 8 | Detects if a function has ``@overload`` or ``@typing.overload`` decorators. 9 | 10 | It is useful, because ``@overload`` function defs 11 | have slightly different rules: for example, they do not count as real defs 12 | in complexity rules. 13 | """ 14 | for decorator in function.decorator_list: 15 | is_partial_name = ( 16 | isinstance(decorator, ast.Name) and decorator.id == 'overload' 17 | ) 18 | is_full_name = ( 19 | isinstance(decorator, ast.Attribute) 20 | and decorator.attr == 'overload' 21 | and isinstance(decorator.value, ast.Name) 22 | and decorator.value.id == 'typing' 23 | ) 24 | if is_partial_name or is_full_name: 25 | return True 26 | return False 27 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/imports.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import NamedTuple, final 3 | 4 | from wemake_python_styleguide import constants 5 | from wemake_python_styleguide.logic.naming import logical 6 | from wemake_python_styleguide.types import AnyImport 7 | 8 | 9 | @final 10 | class ImportedObjectInfo(NamedTuple): 11 | """Information about imported object.""" 12 | 13 | module: str 14 | node: AnyImport 15 | 16 | 17 | def get_module_name(node: ast.ImportFrom) -> str: 18 | """ 19 | Returns module name for any ``ImportFrom``. 20 | 21 | Handles all corner cases, including: 22 | - `from . import a` -> `.` 23 | - `from ..sub import b` -> `..sub` 24 | """ 25 | return '{}{}'.format( 26 | '.' * node.level, 27 | node.module or '', 28 | ) 29 | 30 | 31 | def is_vague_import(name: str) -> bool: 32 | """ 33 | Tells whether this import name is vague or not. 34 | 35 | >>> is_vague_import('a') 36 | True 37 | 38 | >>> is_vague_import('from_model') 39 | True 40 | 41 | >>> is_vague_import('dumps') 42 | True 43 | 44 | >>> is_vague_import('regular') 45 | False 46 | 47 | """ 48 | blacklisted = name in constants.VAGUE_IMPORTS_BLACKLIST 49 | with_from_or_to = name.startswith(('from_', 'to_')) 50 | too_short = logical.is_too_short_name(name, 2, trim=True) 51 | return blacklisted or with_from_or_to or too_short 52 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/keywords.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import TypeAlias 3 | 4 | from wemake_python_styleguide.logic.nodes import get_context 5 | 6 | _ReturningNodes: TypeAlias = list[ast.Return | ast.Yield] 7 | 8 | 9 | def returning_nodes( 10 | node: ast.AST, 11 | returning_type: type[ast.Return] | type[ast.Yield], 12 | ) -> tuple[_ReturningNodes, bool]: 13 | """Returns ``return`` or ``yield`` nodes with values.""" 14 | returns: _ReturningNodes = [] 15 | has_values = False 16 | for sub_node in ast.walk(node): 17 | context_node = get_context(sub_node) 18 | if isinstance(sub_node, returning_type) and context_node == node: 19 | if sub_node.value: # type: ignore 20 | has_values = True 21 | returns.append(sub_node) # type: ignore 22 | return returns, has_values 23 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/loops.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.compat.aliases import ForNodes 4 | from wemake_python_styleguide.types import AnyLoop, AnyNodes 5 | 6 | 7 | def _does_loop_contain_node( 8 | loop: AnyLoop | None, 9 | to_check: ast.AST, 10 | ) -> bool: 11 | """ 12 | Helper function to check for break statement in a nested loop. 13 | 14 | If a loop contains a loop with a break, this ensures that 15 | we don't count the outside loop as having a break. 16 | """ 17 | if loop is None: 18 | return False 19 | 20 | return any(to_check is inner_node for inner_node in ast.walk(loop)) 21 | 22 | 23 | def has_break( 24 | node: AnyLoop, 25 | *, 26 | break_nodes: AnyNodes, 27 | ) -> bool: 28 | """Checks whether loop contains a break statement.""" 29 | closest_loop = None 30 | 31 | for subnode in ast.walk(node): 32 | if isinstance(subnode, (*ForNodes, ast.While)) and subnode is not node: 33 | closest_loop = subnode 34 | 35 | if isinstance(subnode, break_nodes): 36 | is_nested_break = _does_loop_contain_node( 37 | closest_loop, 38 | subnode, 39 | ) 40 | if not is_nested_break: 41 | return True 42 | return False 43 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/operators.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.logic.nodes import get_parent 4 | 5 | 6 | def unwrap_unary_node(node: ast.AST) -> ast.AST: 7 | """ 8 | Returns a real unwrapped node from the unary wrapper. 9 | 10 | It recursively unwraps any level of unary operators. 11 | Returns the node itself if it is not wrapped in unary operator. 12 | """ 13 | while True: 14 | if not isinstance(node, ast.UnaryOp): 15 | return node 16 | node = node.operand 17 | 18 | 19 | def get_parent_ignoring_unary(node: ast.AST) -> ast.AST | None: 20 | """ 21 | Returns real parent ignoring proxy unary parent level. 22 | 23 | What can go wrong? 24 | 25 | 1. Number can be negative: ``x = -1``, 26 | so ``1`` has ``UnaryOp`` as parent, but should return ``Assign`` 27 | 2. Some values can be negated: ``x = --some``, 28 | so ``some`` has ``UnaryOp`` as parent, but should return ``Assign`` 29 | 30 | """ 31 | while True: 32 | parent = get_parent(node) 33 | if parent is None or not isinstance(parent, ast.UnaryOp): 34 | return parent 35 | node = parent 36 | 37 | 38 | def count_unary_operator( 39 | node: ast.AST, 40 | operator: type[ast.unaryop], 41 | amount: int = 0, 42 | ) -> int: 43 | """Returns amount of unary operators matching input.""" 44 | parent = get_parent(node) 45 | if parent is None or not isinstance(parent, ast.UnaryOp): 46 | return amount 47 | if isinstance(parent.op, operator): 48 | return count_unary_operator(parent, operator, amount + 1) 49 | return count_unary_operator(parent, operator, amount) 50 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/pattern_matching.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from collections.abc import Iterable 3 | 4 | from wemake_python_styleguide.logic.nodes import get_parent 5 | from wemake_python_styleguide.logic.tree import ( 6 | operators, 7 | ) 8 | from wemake_python_styleguide.logic.walk import get_subnodes_by_type 9 | from wemake_python_styleguide.logic.walrus import get_assigned_expr 10 | 11 | 12 | def get_explicit_as_names( 13 | node: ast.Match, 14 | ) -> Iterable[ast.MatchAs]: 15 | """ 16 | Returns variable names defined as ``case ... as var_name``. 17 | 18 | Does not return variables defined as ``case var_name``. 19 | Or in any other forms. 20 | """ 21 | for match_as in get_subnodes_by_type(node, ast.MatchAs): 22 | if ( 23 | isinstance(get_parent(match_as), ast.match_case) 24 | and match_as.pattern 25 | and match_as.name 26 | ): 27 | yield match_as 28 | 29 | 30 | def is_constant_subject(condition: ast.AST | list[ast.expr]) -> bool: 31 | """Detect constant subjects for `ast.Match` nodes.""" 32 | if isinstance(condition, list): 33 | return all(is_constant_subject(node) for node in condition) 34 | node = operators.unwrap_unary_node(get_assigned_expr(condition)) 35 | if isinstance(node, ast.Constant): 36 | return True 37 | if isinstance(node, ast.Tuple | ast.List | ast.Set): 38 | return is_constant_subject(node.elts) 39 | if isinstance(node, ast.Dict): 40 | return ( 41 | not any(dict_key is None for dict_key in node.keys) 42 | and is_constant_subject([ 43 | dict_key for dict_key in node.keys if dict_key is not None 44 | ]) 45 | and is_constant_subject(node.values) 46 | ) 47 | return False 48 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/recursion.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.logic.nodes import get_context 4 | from wemake_python_styleguide.logic.tree.functions import given_function_called 5 | from wemake_python_styleguide.types import AnyFunctionDef 6 | 7 | 8 | def _is_self_call(func: AnyFunctionDef, node: ast.AST) -> bool: 9 | return ( 10 | isinstance(node, ast.Call) 11 | and isinstance(node.func, ast.Attribute) 12 | and bool(given_function_called(node, {f'self.{func.name}'})) 13 | ) 14 | 15 | 16 | def _check_method_recursion(func: AnyFunctionDef) -> bool: 17 | return bool([node for node in ast.walk(func) if _is_self_call(func, node)]) 18 | 19 | 20 | def _check_function_recursion(func: AnyFunctionDef) -> bool: 21 | return bool( 22 | [ 23 | node 24 | for node in ast.walk(func) 25 | if isinstance(node, ast.Call) 26 | and given_function_called(node, {func.name}) 27 | ], 28 | ) 29 | 30 | 31 | def has_recursive_calls(func: AnyFunctionDef) -> bool: 32 | """ 33 | Determines whether function has recursive calls or not. 34 | 35 | Does not work for methods. 36 | """ 37 | if isinstance(get_context(func), ast.ClassDef): 38 | return _check_method_recursion(func) 39 | return _check_function_recursion(func) 40 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/slices.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.logic import source 4 | 5 | 6 | def is_same_slice( 7 | iterable: str, 8 | target: str, 9 | node: ast.Subscript, 10 | ) -> bool: 11 | """Used to tell when slice is identical to some pair of name/index.""" 12 | return ( 13 | source.node_to_string(node.value) == iterable 14 | and source.node_to_string(node.slice) == target 15 | ) 16 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/strings.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | 4 | def is_doc_string(node: ast.AST) -> bool: 5 | """ 6 | Tells whether or not the given node is a docstring. 7 | 8 | We call docstrings any string nodes that are placed right after 9 | function, class, or module definition. 10 | """ 11 | if not isinstance(node, ast.Expr): 12 | return False 13 | return isinstance(node.value, ast.Constant) and isinstance( 14 | node.value.value, 15 | str, 16 | ) 17 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/tree/stubs.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.types import AnyFunctionDef 4 | 5 | 6 | def is_stub(node: AnyFunctionDef) -> bool: 7 | """ 8 | Checks if a function is a stub. 9 | 10 | A function (or method) is considered to be a stub if it contains: 11 | - only a docstring 12 | - only an Ellipsis statement (i.e. `...`) 13 | - only a `raise` statement 14 | - a docstring + an Ellipsis statement 15 | - a docstring + a `raise` statement 16 | """ 17 | first_node = node.body[0] 18 | if ( 19 | isinstance(first_node, ast.Expr) 20 | and isinstance(first_node.value, ast.Constant) 21 | and isinstance(first_node.value.value, str) 22 | ): 23 | return _is_stub_with_docstring(node) 24 | return _is_stub_without_docstring(first_node) 25 | 26 | 27 | def _is_stub_with_docstring(node: AnyFunctionDef) -> bool: 28 | statements_in_body = len(node.body) 29 | if statements_in_body == 1: 30 | return True 31 | if statements_in_body == 2: 32 | second_node = node.body[1] 33 | return _is_ellipsis(second_node) or isinstance(second_node, ast.Raise) 34 | return False 35 | 36 | 37 | def _is_stub_without_docstring(node: ast.AST) -> bool: 38 | return _is_ellipsis(node) or isinstance(node, ast.Raise) 39 | 40 | 41 | def _is_ellipsis(node: ast.AST) -> bool: 42 | return ( 43 | isinstance(node, ast.Expr) 44 | and isinstance(node.value, ast.Constant) 45 | and isinstance(node.value.value, type(...)) 46 | ) 47 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/walk.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from collections.abc import Iterator 3 | from typing import TypeAlias, TypeVar 4 | 5 | from wemake_python_styleguide.logic.nodes import get_parent 6 | from wemake_python_styleguide.types import AnyNodes 7 | 8 | _SubnodeType = TypeVar('_SubnodeType', bound=ast.AST) 9 | _IsInstanceContainer: TypeAlias = AnyNodes | type 10 | 11 | 12 | def is_contained( 13 | node: ast.AST, 14 | to_check: _IsInstanceContainer, 15 | ) -> bool: 16 | """ 17 | Checks whether node does contain given subnode types. 18 | 19 | Goes down by the tree to check all children. 20 | """ 21 | return any(isinstance(child, to_check) for child in ast.walk(node)) 22 | 23 | 24 | def get_closest_parent( 25 | node: ast.AST, 26 | parents: _IsInstanceContainer, 27 | ) -> ast.AST | None: 28 | """Returns the closes parent of a node of requested types.""" 29 | parent = get_parent(node) 30 | while True: 31 | if parent is None: 32 | return None 33 | if isinstance(parent, parents): 34 | return parent 35 | parent = get_parent(parent) 36 | 37 | 38 | def is_contained_by(node: ast.AST, container: ast.AST) -> bool: 39 | """ 40 | Tells you if a node is contained by a given container. 41 | 42 | Goes up by the tree of ``node`` to check all parents. 43 | Works with specific instances. 44 | """ 45 | parent = get_parent(node) 46 | while True: 47 | if parent is None: 48 | return False 49 | if parent == container: 50 | return True 51 | parent = get_parent(parent) 52 | 53 | 54 | def get_subnodes_by_type( 55 | node: ast.AST, 56 | subnodes_type: type[_SubnodeType], 57 | ) -> Iterator[_SubnodeType]: 58 | """Returns the list of subnodes of given node with given subnode type.""" 59 | for child in ast.walk(node): 60 | if isinstance(child, subnodes_type): 61 | yield child 62 | -------------------------------------------------------------------------------- /wemake_python_styleguide/logic/walrus.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | 4 | def get_assigned_expr(node: ast.AST) -> ast.AST: 5 | """ 6 | Helper function to retrieve assigned value from ``NamedExpr``. 7 | 8 | If the node is an actual ``NamedExpr``, the assigned value will be returned. 9 | For other node type, the original node is returned. 10 | """ 11 | if isinstance(node, ast.NamedExpr): 12 | return node.value 13 | return node 14 | -------------------------------------------------------------------------------- /wemake_python_styleguide/options/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/options/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/presets/__init__.py: -------------------------------------------------------------------------------- 1 | """See :term:`preset` in the docs.""" 2 | -------------------------------------------------------------------------------- /wemake_python_styleguide/presets/topics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/presets/topics/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/presets/topics/classes.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from wemake_python_styleguide.visitors.ast.classes import ( 4 | attributes, 5 | classdef, 6 | methods, 7 | ) 8 | 9 | #: Used to store all classes related visitors to be later passed to checker: 10 | PRESET: Final = ( 11 | classdef.WrongClassDefVisitor, 12 | classdef.WrongClassBodyVisitor, 13 | attributes.ClassAttributeVisitor, 14 | attributes.WrongSlotsVisitor, 15 | methods.WrongMethodVisitor, 16 | methods.ClassMethodOrderVisitor, 17 | methods.BuggySuperCallVisitor, 18 | classdef.ConsecutiveDefaultTypeVarsVisitor, 19 | ) 20 | -------------------------------------------------------------------------------- /wemake_python_styleguide/presets/topics/complexity.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from wemake_python_styleguide.visitors.ast.complexity import ( # noqa: WPS235 4 | access, 5 | annotations, 6 | calls, 7 | classes, 8 | counts, 9 | function, 10 | imports, 11 | jones, 12 | nested, 13 | offset, 14 | overuses, 15 | pm, 16 | ) 17 | 18 | #: Used to store all complexity related visitors to be later passed to checker: 19 | PRESET: Final = ( 20 | function.FunctionComplexityVisitor, 21 | function.CognitiveComplexityVisitor, 22 | imports.ImportMembersVisitor, 23 | jones.JonesComplexityVisitor, 24 | nested.NestedComplexityVisitor, 25 | offset.OffsetVisitor, 26 | counts.ModuleMembersVisitor, 27 | counts.ConditionsVisitor, 28 | counts.ElifVisitor, 29 | counts.TryExceptVisitor, 30 | counts.ReturnLikeStatementTupleVisitor, 31 | counts.TupleUnpackVisitor, 32 | counts.TypeParamsVisitor, 33 | classes.ClassComplexityVisitor, 34 | classes.MethodMembersVisitor, 35 | overuses.StringOveruseVisitor, 36 | overuses.ExpressionOveruseVisitor, 37 | access.AccessVisitor, 38 | calls.CallChainsVisitor, 39 | annotations.AnnotationComplexityVisitor, 40 | pm.MatchSubjectsVisitor, 41 | pm.MatchCasesVisitor, 42 | ) 43 | -------------------------------------------------------------------------------- /wemake_python_styleguide/presets/topics/naming.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from wemake_python_styleguide.visitors.ast.naming import validation, variables 4 | 5 | #: Used to store all naming related visitors to be later passed to checker: 6 | PRESET: Final = ( 7 | validation.WrongNameVisitor, 8 | variables.WrongModuleMetadataVisitor, 9 | variables.UnusedVariableUsageVisitor, 10 | variables.UnusedVariableDefinitionVisitor, 11 | ) 12 | -------------------------------------------------------------------------------- /wemake_python_styleguide/presets/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/presets/types/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/presets/types/file_tokens.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from wemake_python_styleguide.visitors.tokenize import ( 4 | comments, 5 | conditions, 6 | primitives, 7 | statements, 8 | syntax, 9 | ) 10 | 11 | #: Used to store all token related visitors to be later passed to checker: 12 | PRESET: Final = ( 13 | comments.WrongCommentVisitor, 14 | comments.ShebangVisitor, 15 | comments.NoqaVisitor, 16 | comments.EmptyCommentVisitor, 17 | comments.CommentInFormattedStringVisitor, 18 | syntax.WrongKeywordTokenVisitor, 19 | primitives.WrongNumberTokenVisitor, 20 | primitives.WrongStringTokenVisitor, 21 | statements.MultilineStringVisitor, 22 | conditions.IfElseVisitor, 23 | primitives.MultilineFormattedStringTokenVisitor, 24 | ) 25 | -------------------------------------------------------------------------------- /wemake_python_styleguide/presets/types/filename.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from wemake_python_styleguide.visitors.filenames.module import ( 4 | WrongModuleNameVisitor, 5 | ) 6 | 7 | #: Here we define all filename-based visitors. 8 | PRESET: Final = (WrongModuleNameVisitor,) 9 | -------------------------------------------------------------------------------- /wemake_python_styleguide/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/py.typed -------------------------------------------------------------------------------- /wemake_python_styleguide/transformations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/transformations/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/transformations/ast/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/transformations/ast/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/transformations/ast/enhancements.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import Final 3 | 4 | from wemake_python_styleguide.compat.aliases import FunctionNodes 5 | from wemake_python_styleguide.logic.nodes import get_parent 6 | from wemake_python_styleguide.types import ContextNodes 7 | 8 | _CONTEXTS: Final = ( 9 | ast.Module, 10 | ast.ClassDef, 11 | *FunctionNodes, 12 | ) 13 | 14 | 15 | def set_node_context(tree: ast.AST) -> ast.AST: 16 | """ 17 | Used to set proper context to all nodes. 18 | 19 | What we call "a context"? 20 | Context is where exactly this node belongs on a global level. 21 | 22 | Example: 23 | .. code:: python 24 | 25 | if some_value > 2: 26 | test = 'passed' 27 | 28 | Despite the fact ``test`` variable has ``Assign`` as it parent 29 | it will have ``Module`` as a context. 30 | 31 | What contexts do we respect? 32 | 33 | - :py:class:`ast.Module` 34 | - :py:class:`ast.ClassDef` 35 | - :py:class:`ast.FunctionDef` and :py:class:`ast.AsyncFunctionDef` 36 | 37 | .. versionchanged:: 0.8.1 38 | 39 | """ 40 | for statement in ast.walk(tree): 41 | current_context = _find_context(statement, _CONTEXTS) 42 | setattr(statement, 'wps_context', current_context) # noqa: B010 43 | return tree 44 | 45 | 46 | def _find_context( 47 | node: ast.AST, 48 | contexts: tuple[type[ContextNodes], ...], 49 | ) -> ast.AST | None: 50 | """ 51 | We changed how we find and assign contexts in 0.8.1 version. 52 | 53 | It happened because of the bug #520 54 | See: https://github.com/wemake-services/wemake-python-styleguide/issues/520 55 | """ 56 | parent = get_parent(node) 57 | if parent is None: 58 | return None 59 | if isinstance(parent, contexts): 60 | return parent 61 | return _find_context(parent, contexts) 62 | -------------------------------------------------------------------------------- /wemake_python_styleguide/transformations/ast_tree.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from wemake_python_styleguide.transformations.ast.enhancements import ( 4 | set_node_context, 5 | ) 6 | 7 | 8 | def _set_parent(tree: ast.AST) -> ast.AST: 9 | """ 10 | Sets parents for all nodes that do not have this prop. 11 | 12 | This step is required due to how `flake8` works. 13 | It does not set the same properties as `ast` module. 14 | 15 | This function was the cause of `issue-112`. Twice. 16 | Since the ``0.6.1`` we use ``'wps_parent'`` with a prefix. 17 | This should fix the issue with conflicting plugins. 18 | 19 | .. versionchanged:: 0.0.11 20 | .. versionchanged:: 0.6.1 21 | 22 | """ 23 | for statement in ast.walk(tree): 24 | for child in ast.iter_child_nodes(statement): 25 | setattr(child, 'wps_parent', statement) # noqa: B010 26 | return tree 27 | 28 | 29 | def transform(tree: ast.AST) -> ast.AST: 30 | """ 31 | Mutates the given ``ast`` tree. 32 | 33 | Applies all possible transformations. 34 | 35 | Ordering: 36 | - initial ones 37 | - bugfixes 38 | - enhancements 39 | 40 | """ 41 | pipeline = ( 42 | # Initial, should be the first ones, ordering inside is important: 43 | _set_parent, 44 | # Enhancements, order is not important: 45 | set_node_context, 46 | ) 47 | 48 | for transformation in pipeline: 49 | tree = transformation(tree) 50 | return tree 51 | -------------------------------------------------------------------------------- /wemake_python_styleguide/version.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Final 3 | 4 | from wemake_python_styleguide.compat.packaging import get_version 5 | 6 | #: This is a package name. It is basically the name of the root folder. 7 | pkg_name: Final = str(Path(__file__).parent.name) 8 | 9 | #: We store the version number inside the `pyproject.toml`. 10 | pkg_version: Final = get_version(pkg_name) 11 | -------------------------------------------------------------------------------- /wemake_python_styleguide/violations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/violations/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/violations/system.py: -------------------------------------------------------------------------------- 1 | """ 2 | These checks ensures that our internal checks passes. 3 | 4 | For example, we can report violations from this group 5 | when some exception occur during the linting process 6 | or some dependencies are missing. 7 | 8 | .. currentmodule:: wemake_python_styleguide.violations.system 9 | 10 | Summary 11 | ------- 12 | 13 | .. autosummary:: 14 | :nosignatures: 15 | 16 | InternalErrorViolation 17 | 18 | Respect your objects 19 | -------------------- 20 | 21 | .. autoclass:: InternalErrorViolation 22 | 23 | """ 24 | 25 | from typing import final 26 | 27 | from wemake_python_styleguide.violations.base import SimpleViolation 28 | 29 | 30 | @final 31 | class InternalErrorViolation(SimpleViolation): 32 | """ 33 | Happens when we get unhandled exception during the linting process. 34 | 35 | All this violations should be reported to the main issue tracker. 36 | We ideally should not produce these violations at all. 37 | 38 | See also: 39 | https://github.com/wemake-services/wemake-python-styleguide/issues 40 | 41 | .. versionadded:: 0.13.0 42 | 43 | """ 44 | 45 | error_template = ( 46 | 'Internal error happened, see log. Please, take some time to report it' 47 | ) 48 | code = 0 49 | -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/visitors/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/ast/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/visitors/ast/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/ast/classes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/visitors/ast/classes/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/ast/complexity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/visitors/ast/complexity/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/ast/complexity/calls.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from itertools import takewhile 3 | from typing import final 4 | 5 | from wemake_python_styleguide.logic.tree.calls import parts 6 | from wemake_python_styleguide.violations.complexity import ( 7 | TooLongCallChainViolation, 8 | ) 9 | from wemake_python_styleguide.visitors.base import BaseNodeVisitor 10 | 11 | 12 | @final 13 | class CallChainsVisitor(BaseNodeVisitor): 14 | """Counts number of consecutive calls.""" 15 | 16 | def __init__(self, *args, **kwargs) -> None: 17 | """Keeps visited calls to not visit them again.""" 18 | super().__init__(*args, **kwargs) 19 | self._visited_calls: set[ast.Call] = set() 20 | 21 | def visit_Call(self, node: ast.Call) -> None: 22 | """Checks number of function calls.""" 23 | self._check_consecutive_call_number(node) 24 | self.generic_visit(node) 25 | 26 | def _is_call(self, node: ast.AST) -> bool: 27 | return isinstance(node, ast.Call) 28 | 29 | def _check_consecutive_call_number(self, node: ast.Call) -> None: 30 | if node in self._visited_calls: 31 | return 32 | 33 | consecutive_calls = set( 34 | takewhile( 35 | self._is_call, 36 | parts(node), 37 | ), 38 | ) 39 | 40 | self._visited_calls.update(consecutive_calls) 41 | num_of_calls = len(consecutive_calls) 42 | 43 | if num_of_calls > self.options.max_call_level: 44 | self.add_violation( 45 | TooLongCallChainViolation( 46 | node, 47 | text=str(num_of_calls), 48 | baseline=self.options.max_call_level, 49 | ), 50 | ) 51 | -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/ast/complexity/pm.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import final 3 | 4 | from wemake_python_styleguide.violations import complexity 5 | from wemake_python_styleguide.visitors.base import BaseNodeVisitor 6 | 7 | 8 | @final 9 | class MatchSubjectsVisitor(BaseNodeVisitor): 10 | """Finds excessive match subjects in `match` statements.""" 11 | 12 | def visit_Match(self, node: ast.Match) -> None: 13 | """Finds all `match` statements and checks their subjects.""" 14 | self._check_match_subjects_count(node) 15 | self.generic_visit(node) 16 | 17 | def _check_match_subjects_count(self, node: ast.Match) -> None: 18 | if not isinstance(node.subject, ast.Tuple): 19 | return 20 | if len(node.subject.elts) <= self.options.max_match_subjects: 21 | return 22 | self.add_violation( 23 | complexity.TooManyMatchSubjectsViolation( 24 | node, 25 | text=str(len(node.subject.elts)), 26 | baseline=self.options.max_match_subjects, 27 | ) 28 | ) 29 | 30 | 31 | @final 32 | class MatchCasesVisitor(BaseNodeVisitor): 33 | """Finds excessive match cases in `match` statements.""" 34 | 35 | def visit_Match(self, node: ast.Match) -> None: 36 | """Finds all `match` statements and checks their cases.""" 37 | self._check_match_cases_count(node) 38 | self.generic_visit(node) 39 | 40 | def _check_match_cases_count(self, node: ast.Match) -> None: 41 | if len(node.cases) > self.options.max_match_cases: 42 | self.add_violation( 43 | violation=complexity.TooManyMatchCaseViolation( 44 | text=str(len(node.cases)), 45 | node=node, 46 | baseline=self.options.max_match_cases, 47 | ), 48 | ) 49 | -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/ast/iterables.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import ClassVar, final 3 | 4 | from wemake_python_styleguide.logic.nodes import get_parent 5 | from wemake_python_styleguide.types import AnyNodes 6 | from wemake_python_styleguide.violations.consistency import ( 7 | IterableUnpackingViolation, 8 | ) 9 | from wemake_python_styleguide.visitors import base 10 | 11 | 12 | @final 13 | class IterableUnpackingVisitor(base.BaseNodeVisitor): 14 | """Checks iterables unpacking.""" 15 | 16 | _unpackable_iterable_parent_types: ClassVar[AnyNodes] = ( 17 | ast.List, 18 | ast.Set, 19 | ast.Tuple, 20 | ) 21 | 22 | def visit_Starred(self, node: ast.Starred) -> None: 23 | """Checks iterable's unpacking.""" 24 | self._check_unnecessary_iterable_unpacking(node) 25 | self.generic_visit(node) 26 | 27 | def _check_unnecessary_iterable_unpacking(self, node: ast.Starred) -> None: 28 | parent = get_parent(node) 29 | if not isinstance(parent, self._unpackable_iterable_parent_types): 30 | return 31 | if len(getattr(parent, 'elts', [])) != 1: 32 | return 33 | 34 | container = get_parent(parent) 35 | if isinstance(container, ast.Subscript): # pragma: >=3.11 cover 36 | # We ignore cases like `Tuple[*Shape]`, because it is a type 37 | # annotation which should be used like this. 38 | # It is only possible for Python 3.11+ 39 | return 40 | self.add_violation(IterableUnpackingViolation(node)) 41 | -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/ast/naming/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/visitors/ast/naming/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/ast/pm.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import ClassVar, final 3 | 4 | from wemake_python_styleguide.logic.tree import pattern_matching 5 | from wemake_python_styleguide.types import AnyNodes 6 | from wemake_python_styleguide.violations.refactoring import ( 7 | ExtraMatchSubjectSyntaxViolation, 8 | ) 9 | from wemake_python_styleguide.visitors.base import BaseNodeVisitor 10 | 11 | 12 | @final 13 | class MatchSubjectVisitor(BaseNodeVisitor): 14 | """Restricts the incorrect subjects in PM.""" 15 | 16 | _forbidden_syntax: ClassVar[AnyNodes] = ( 17 | ast.Dict, 18 | ast.Set, 19 | ast.List, 20 | ast.Tuple, 21 | ) 22 | 23 | def visit_Match(self, node: ast.Match) -> None: 24 | """Visits `Match` nodes and checks their internals.""" 25 | self._check_extra_syntax(node.subject) 26 | self.generic_visit(node) 27 | 28 | def _check_extra_syntax(self, node: ast.expr) -> None: 29 | if not isinstance(node, self._forbidden_syntax): 30 | return 31 | if pattern_matching.is_constant_subject(node): 32 | return # raises another violation in a different place 33 | if isinstance(node, ast.Tuple) and len(node.elts) > 1: 34 | return 35 | self.add_violation(ExtraMatchSubjectSyntaxViolation(node)) 36 | -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/decorators.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | _DefinedType = TypeVar('_DefinedType') 5 | 6 | 7 | def _modify_class( 8 | cls: type[_DefinedType], 9 | original: str, 10 | aliases: tuple[str, ...], 11 | ) -> type[_DefinedType]: 12 | original_handler = getattr(cls, original, None) 13 | if original_handler is None: 14 | raise AttributeError( 15 | f'Aliased attribute {original} does not exist', 16 | ) 17 | 18 | for method_alias in aliases: 19 | if getattr(cls, method_alias, None): 20 | raise AttributeError( 21 | f'Alias {method_alias} already exists', 22 | ) 23 | setattr(cls, method_alias, original_handler) 24 | return cls 25 | 26 | 27 | def alias( 28 | original: str, 29 | aliases: tuple[str, ...], 30 | ) -> Callable[[type[_DefinedType]], type[_DefinedType]]: 31 | """ 32 | Decorator to alias handlers. 33 | 34 | Why do we need it? 35 | Because there are cases when we need to use the same method to 36 | handle different nodes types. 37 | 38 | We can just create aliases like ``visit_Import = visit_ImportFrom``, 39 | but it looks verbose and ugly. 40 | """ 41 | all_names = (*aliases, original) 42 | if len(all_names) != len(set(all_names)): 43 | raise ValueError('Found duplicate aliases') 44 | 45 | def decorator(cls: type[_DefinedType]) -> type[_DefinedType]: 46 | return _modify_class(cls, original, aliases) 47 | 48 | return decorator 49 | -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/filenames/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/visitors/filenames/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/tokenize/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/cf795e92de7e4baccaf641665d6fc5beddb64a04/wemake_python_styleguide/visitors/tokenize/__init__.py -------------------------------------------------------------------------------- /wemake_python_styleguide/visitors/tokenize/syntax.py: -------------------------------------------------------------------------------- 1 | import tokenize 2 | from typing import final 3 | 4 | from wemake_python_styleguide.violations.consistency import ( 5 | LineCompriseCarriageReturnViolation, 6 | ) 7 | from wemake_python_styleguide.visitors.base import BaseTokenVisitor 8 | from wemake_python_styleguide.visitors.decorators import alias 9 | 10 | 11 | @final 12 | @alias( 13 | 'visit_any_newline', 14 | ( 15 | 'visit_newline', 16 | 'visit_nl', 17 | ), 18 | ) 19 | class WrongKeywordTokenVisitor(BaseTokenVisitor): 20 | """Visits keywords and finds violations related to their usage.""" 21 | 22 | def visit_any_newline(self, token: tokenize.TokenInfo) -> None: 23 | r"""Checks ``\r`` (carriage return) in line breaks.""" 24 | self._check_line_comprise_carriage_return(token) 25 | 26 | def _check_line_comprise_carriage_return( 27 | self, 28 | token: tokenize.TokenInfo, 29 | ) -> None: 30 | if '\r' in token.string: 31 | self.add_violation(LineCompriseCarriageReturnViolation(token)) 32 | --------------------------------------------------------------------------------