def test_simulate_parsing_error_when_saving(update_file, tmp_path): """Simulate a parsing error when saving an INI file.""" update_file.side_effect = ParsingError( source="simulating a captured error") original_file = """ [flake8] existing = value """ ProjectMock(tmp_path).style(f""" ["{SETUP_CFG}".flake8] new = "value" """).setup_cfg(original_file).api_fix().assert_violations( Fuss( True, SETUP_CFG, 324, ": section [flake8] has some missing key/value pairs. Use this:", """ [flake8] new = value """, ), Fuss( False, SETUP_CFG, Violations.PARSING_ERROR.code, ": parsing error (ParsingError): Source contains parsing errors: 'simulating a captured error'", ), ).assert_file_contents(SETUP_CFG, original_file)
def test_default_style_is_applied(project_default, datadir): """Test if the default style is applied on an empty project.""" # TODO: test: nitpick preset in a generic way, preparing for other presets to come preset_dir = datadir / "preset" / "nitpick" expected_setup_cfg = (preset_dir / "setup.cfg").read_text() expected_editor_config = (preset_dir / ".editorconfig").read_text() expected_tox_ini = (preset_dir / "tox.ini").read_text() expected_pylintrc = (preset_dir / ".pylintrc").read_text() project_default.api_check_then_fix( Fuss(True, SETUP_CFG, 321, " was not found. Create it with this content:", expected_setup_cfg), Fuss(True, EDITOR_CONFIG, 321, " was not found. Create it with this content:", expected_editor_config), Fuss(True, TOX_INI, 321, " was not found. Create it with this content:", expected_tox_ini), Fuss(True, PYLINTRC, 321, " was not found. Create it with this content:", expected_pylintrc), partial_names=[SETUP_CFG, EDITOR_CONFIG, TOX_INI, PYLINTRC], ).assert_file_contents( SETUP_CFG, expected_setup_cfg, EDITOR_CONFIG, expected_editor_config, TOX_INI, expected_tox_ini, PYLINTRC, expected_pylintrc, )
def test_absent_files(tmp_path): """Test absent files from the style configuration.""" ProjectMock(tmp_path).style(""" [nitpick.files.absent] xxx = "Remove this" yyy = "Remove that" """).touch_file("xxx").touch_file("yyy").api_check_then_fix( Fuss(False, "xxx", 104, " should be deleted: Remove this"), Fuss(False, "yyy", 104, " should be deleted: Remove that"), )
def test_present_files(tmp_path): """Test present files from the style configuration.""" ProjectMock(tmp_path).style(""" [nitpick.files.present] ".editorconfig" = "Create this file" ".env" = "" "another-file.txt" = "" """).api_check_then_fix( Fuss(False, ".editorconfig", 103, " should exist: Create this file"), Fuss(False, ".env", 103, " should exist"), Fuss(False, "another-file.txt", 103, " should exist"), )
def test_missing_different_values_with_contains_json_without_contains_keys(tmp_path, datadir): """Test missing and different values with "contains_json", without "contains_keys".""" ProjectMock(tmp_path).style(datadir / "3-style.toml").save_file( "my.json", '{"name":"myproject","formatting":{"on.the":"actual file"}}' ).api_check_then_fix( Fuss( True, "my.json", SharedViolations.MISSING_VALUES.code + JsonPlugin.violation_base_code, " has missing values:", """ { "formatting": { "doesnt": "matter", "here": true }, "some.dotted.root.key": { "content": [ "should", "be", "here" ], "dotted.subkeys": [ "should be preserved", { "complex.weird.sub": { "objects": true }, "even.with": 1 } ], "valid": "JSON" } } """, ), Fuss( True, "my.json", SharedViolations.DIFFERENT_VALUES.code + JsonPlugin.violation_base_code, " has different values. Use this:", """ { "formatting": { "on.the": "config file" } } """, ), ).assert_file_contents( "my.json", datadir / "3-expected.json" ).api_check_then_fix()
def test_missing_different_values(tmp_path, datadir): """Test different and missing values on any YAML.""" filename = "me/deep/rooted.yaml" project = ProjectMock(tmp_path).save_file(filename, datadir / "existing-actual.yaml") project.style(datadir / "existing-desired.toml").api_check_then_fix( Fuss( True, filename, 369, " has different values. Use this:", """ python: version: '3.9' """, ), Fuss( True, filename, 368, " has missing values:", """ python: install: - extra_requirements: - some - nice - package root_key: a_dict: - c: '3.1' - b: 2 - a: string value a_nested: list: - 0 - 2 - 1 int: 10 mixed: - lets: ruin: this with: - weird - '1' - crap - second item: also a dict """, ), ).assert_file_contents(filename, datadir / "existing-expected.yaml") project.api_check().assert_violations()
def test_fuss_pretty(fixed): """Test Fuss' pretty formatting.""" examples = [ (Fuss(fixed, "abc.txt", 2, "message"), "abc.txt:1: NIP002 message"), (Fuss(fixed, "abc.txt", 2, "message", "", 15), "abc.txt:15: NIP002 message"), ( Fuss(fixed, "abc.txt", 1, "message", "\tsuggestion\n\t "), f"abc.txt:1: NIP001 message{SUGGESTION_BEGIN}\n\tsuggestion{SUGGESTION_END}", ), (Fuss(fixed, " ", 3, "no filename"), "NIP003 no filename"), ] for fuss, expected in examples: compare(actual=fuss.pretty, expected=dedent(expected))
def test_repo_should_be_added_not_replaced(tmp_path, datadir): """Test a pre-commit repo being added to the list and not replacing an existing repo in the same position.""" ProjectMock(tmp_path).save_file( PRE_COMMIT_CONFIG_YAML, datadir / "uk-actual.yaml").style( datadir / "uk-default.toml").api_check_then_fix( Fuss( True, PRE_COMMIT_CONFIG_YAML, YamlPlugin.violation_base_code + SharedViolations.MISSING_VALUES.code, " has missing values:", """ repos: - repo: https://github.com/myint/autoflake hooks: - id: autoflake args: - --in-place - --remove-all-unused-imports - --remove-unused-variables - --remove-duplicate-keys - --ignore-init-module-imports """, ), ).assert_file_contents(PRE_COMMIT_CONFIG_YAML, datadir / "uk-default-expected.yaml" ).api_check().assert_violations()
def test_overriding_list_key_with_empty_string_restores_default_behaviour( tmp_path, datadir): """Test overriding the default list key with nothing. The default behaviour will be restored, and the new element will be appended at the end of the list. """ ProjectMock(tmp_path).save_file( PRE_COMMIT_CONFIG_YAML, datadir / "uk-actual.yaml").style( datadir / "uk-empty.toml").api_check_then_fix( Fuss( True, PRE_COMMIT_CONFIG_YAML, 368, " has missing values:", """ repos: - repo: https://github.com/myint/autoflake hooks: - id: autoflake args: - --in-place - --remove-all-unused-imports - --remove-unused-variables - --remove-duplicate-keys - --ignore-init-module-imports """, ), ).assert_file_contents( PRE_COMMIT_CONFIG_YAML, datadir / "uk-empty-expected.yaml").api_check().assert_violations()
def test_repo_with_different_key_value_pairs(tmp_path, datadir): """Test a nested dict with different key/value pairs, e.g.: different args or dependencies.""" ProjectMock(tmp_path).save_file( PRE_COMMIT_CONFIG_YAML, datadir / "hook-args.yaml").style( datadir / "hook-args-change.toml").api_check_then_fix( Fuss( True, PRE_COMMIT_CONFIG_YAML, 368, " has missing values:", """ repos: - repo: https://github.com/psf/black hooks: - id: black args: - --safe - --custom - --loud - repo: https://github.com/asottile/blacken-docs hooks: - id: blacken-docs additional_dependencies: - black==22.1 """, )).assert_file_contents( PRE_COMMIT_CONFIG_YAML, datadir / "hook-args-change.yaml").api_check().assert_violations()
def test_wildcard_expression_matches_multiple_keys(tmp_path, datadir): """Test wildcard expressions that match multiple keys. E.g.: any "jobs.*.steps".""" filename = ".github/workflows/anything.yaml" ProjectMock(tmp_path).save_file(filename, datadir / "wildcard-actual.yaml").style( datadir / "wildcard-desired.toml" ).api_check_then_fix( Fuss( True, filename, 368, " has missing values:", """ jobs: build: steps: - name: Checkout uses: actions/checkout@v2 test: steps: - name: Checkout uses: actions/checkout@v2 release: steps: - name: Checkout uses: actions/checkout@v2 """, ), ).assert_file_contents( filename, datadir / "wildcard-expected.yaml" ).api_check().assert_violations()
def test_list_of_dicts_search_missing_element_by_key_and_change_add_element_individually(tmp_path, datadir): """Test list of dicts: search missing element by key and change/add element individually. In GitHub Workflows: steps are searched by name and they should exist. """ filename = ".github/workflows/any-language.yaml" ProjectMock(tmp_path).save_file(filename, datadir / "dict-search-by-key-actual.yaml").style( datadir / "dict-search-by-key-desired.toml" ).api_check_then_fix( Fuss( True, filename, 368, " has missing values:", """ jobs: build: steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install tox run: python -m pip install tox """, ), ).assert_file_contents( filename, datadir / "dict-search-by-key-expected.yaml" ).api_check().assert_violations()
def test_list_of_scalars_only_add_elements_that_do_not_exist(tmp_path, datadir): """Test list of scalars: only add elements that do not exist. Only check if the element is present but don't enforce the whole list to be the same. """ filename = ".github/workflows/python.yaml" ProjectMock(tmp_path).save_file(filename, datadir / "scalar-add-elements-that-do-not-exist-actual.yaml").style( datadir / "scalar-add-elements-that-do-not-exist-desired.toml" ).api_check_then_fix( Fuss( True, filename, 368, " has missing values:", """ jobs: build: strategy: matrix: os: - ubuntu-latest python-version: - '3.7' - '3.10' - '3.11' """, ), ).assert_file_contents( filename, datadir / "scalar-add-elements-that-do-not-exist-expected.yaml" ).api_check().assert_violations()
def test_more_than_one_element_with_the_same_key_only_first_one_will_be_considered(tmp_path, datadir): """Test more than one element with the same key: only the first one will be considered. E.g.: two steps with the same name. """ filename = ".github/workflows/something.yaml" ProjectMock(tmp_path).save_file(filename, datadir / "same-key-actual.yaml").style( datadir / "same-key-desired.toml" ).api_check_then_fix( Fuss( True, filename, 368, " has missing values:", """ jobs: whatever: steps: - name: Same key uses: actions/replacing-duplicated-element@v2 """, ) ).assert_file_contents( filename, datadir / "same-key-expected.yaml" ).api_check().assert_violations()
def assert_violations(self, *expected_violations: Fuss, disclaimer="") -> ProjectMock: """Assert the exact set of violations.""" manual: int = 0 fixed: int = 0 stripped: set[Fuss] = set() for orig in expected_violations: if orig.fixed: fixed += 1 else: manual += 1 # Keep non-breaking space needed by some tests (e.g. YAML) clean_suggestion = dedent(orig.suggestion).strip().replace(NBSP, " ") stripped.add(Fuss(orig.fixed, orig.filename, orig.code, orig.message, clean_suggestion)) dict_difference = compare( expected=[obj.__dict__ for obj in sorted(stripped)], actual=[obj.__dict__ for obj in sorted(self._actual_violations)], raises=False, ) prefix = f"[{disclaimer}] " if disclaimer else "" compare( expected=stripped, actual=self._actual_violations, suffix=f"{prefix}Comparing Fuss objects as dictionaries: {dict_difference}", ) compare(expected=fixed, actual=Reporter.fixed) compare(expected=manual, actual=Reporter.manual) return self
def api_check_then_fix( self, *expected_violations_when_fixing: Fuss, partial_names: Iterable[str] | None = None ) -> ProjectMock: """Assert that check mode does not change files, and that autofix mode changes them. Perform a series of calls and assertions: 1. Call the API in check mode, assert violations, assert files contents were not modified. 2. Call the API in autofix mode and assert violations again. :param expected_violations_when_fixing: Expected violations when "autofix mode" is on. :param partial_names: Names of the files to enforce configs for. :return: ``self`` for method chaining (fluent interface) """ partial_names = partial_names or [] expected_filenames = set() expected_violations_when_checking = [] for orig in expected_violations_when_fixing: expected_filenames.add(orig.filename) expected_violations_when_checking.append( Fuss(False, orig.filename, orig.code, orig.message, orig.suggestion) ) contents_before_check = self.read_multiple_files(expected_filenames) self.api_check(*partial_names).assert_violations(*expected_violations_when_checking, disclaimer="Check") contents_after_check = self.read_multiple_files(expected_filenames) compare(expected=contents_before_check, actual=contents_after_check) return self.api_fix(*partial_names).assert_violations(*expected_violations_when_fixing, disclaimer="Fix")
def test_invalid_configuration_comma_separated_values(tmp_path): """Test an invalid configuration for comma_separated_values.""" ProjectMock(tmp_path).style(f""" ["{SETUP_CFG}".flake8] max-line-length = 85 max-complexity = 12 ignore = "D100,D101,D102,D103,D104,D105,D106,D107,D202,E203,W503" select = "E241,C,E,F,W,B,B9" [nitpick.files."{SETUP_CFG}"] comma_separated_values = ["flake8.ignore", "flake8.exclude"] """).api_check().assert_violations( Fuss( False, SETUP_CFG, 321, " was not found. Create it with this content:", """ [flake8] max-line-length = 85 max-complexity = 12 ignore = D100,D101,D102,D103,D104,D105,D106,D107,D202,E203,W503 select = E241,C,E,F,W,B,B9 """, ))
def test_include_remote_style_from_local_style(tmp_path): """Test include of remote style when there is only a local style.""" remote_style = "https://raw.githubusercontent.com/user/repo/branch/path/to/nitpick-style" url_with_extension = f"{remote_style}{TOML_EXTENSION}" body = """ ["tox.ini".section] key = "value" """ responses.add(responses.GET, url_with_extension, dedent(body), status=200) project = ProjectMock(tmp_path).style(f""" [nitpick.styles] include = [ "{remote_style}" ] """) project.assert_file_contents(TOX_INI, None).api_check_then_fix( Fuss(True, TOX_INI, 321, " was not found. Create it with this content:", "[section]\nkey = value")).assert_file_contents( TOX_INI, """ [section] key = value """, PYPROJECT_TOML, None, )
def test_comma_separated_keys_on_style_file(tmp_path): """Comma separated keys on the style file.""" ProjectMock(tmp_path).style(f""" [nitpick.files."{SETUP_CFG}"] comma_separated_values = ["food.eat", "food.drink"] ["{SETUP_CFG}".food] eat = "salt,ham,eggs" drink = "water,bier,wine" """).setup_cfg(""" [food] eat = spam,eggs,cheese drink = wine , bier , water """).api_check_then_fix( Fuss( True, SETUP_CFG, Violations.MISSING_VALUES_IN_LIST.code, " has missing values in the 'eat' key. Include those values:", """ [food] eat = (...),ham,salt """, )).assert_file_contents( SETUP_CFG, """ [food] eat = spam,eggs,cheese,ham,salt drink = wine , bier , water """, )
def test_suggest_initial_contents(tmp_path, datadir): """Suggest initial contents for missing pre-commit config file.""" warnings.simplefilter("ignore") # "repos.yaml" key ProjectMock(tmp_path).named_style( "isort", datadir / "1-isort.toml").named_style( "black", datadir / "1-black.toml").pyproject_toml(""" [tool.nitpick] style = ["isort", "black"] """).api_check_then_fix( Fuss( True, PRE_COMMIT_CONFIG_YAML, 361, " was not found. Create it with this content:", """ repos: [] """, ), partial_names=[PRE_COMMIT_CONFIG_YAML], ).assert_file_contents( PRE_COMMIT_CONFIG_YAML, """ repos: [] """, )
def test_missing_hook_with_id(tmp_path): """Test missing hook with specific id.""" ProjectMock(tmp_path).style(''' [[".pre-commit-config.yaml".repos]] repo = "other" hooks = """ - id: black name: black entry: black """ ''').pre_commit(""" repos: - repo: other hooks: - id: isort """).api_check_then_fix( Fuss( True, PRE_COMMIT_CONFIG_YAML, 368, " has missing values:", """ repos: - repo: other hooks: "- id: black\\n name: black\\n entry: black\\n" """, ))
def test_style_missing_id_in_hook(tmp_path): """Test style file is missing id in hook. Read the warning on :py:class:`nitpick.plugins.yaml.YamlPlugin`.""" ProjectMock(tmp_path).style(f''' [[".pre-commit-config.yaml".repos]] repo = "another" hooks = """ - name: isort entry: isort -sp {SETUP_CFG} """ ''').pre_commit(""" repos: - repo: another hooks: - id: isort """).api_check_then_fix( Fuss( True, PRE_COMMIT_CONFIG_YAML, 368, " has missing values:", 'repos:\n - repo: another\n hooks: "- name: isort\\n entry: isort -sp setup.cfg\\n"', )).assert_file_contents( PRE_COMMIT_CONFIG_YAML, r""" repos: - repo: another hooks: - id: isort - repo: another hooks: "- name: isort\n entry: isort -sp setup.cfg\n" """, )
def test_missing_repo_key(tmp_path): """Test missing repo key on the style file.""" ProjectMock(tmp_path).style(""" [[".pre-commit-config.yaml".repos]] grepo = "glocal" """).pre_commit(""" repos: - hooks: - id: whatever """).api_check_then_fix( Fuss( True, PRE_COMMIT_CONFIG_YAML, 368, " has missing values:", """ repos: - grepo: glocal """, ), ).assert_file_contents( PRE_COMMIT_CONFIG_YAML, """ repos: - hooks: - id: whatever - grepo: glocal """, )
def test_relative_style_on_urls(tmp_path): """Read styles from relative paths on URLs.""" base_url = "http://www.example.com/sub/folder" mapping = { "main": """ [nitpick.styles] include = "presets/python.toml" """, "presets/python": """ [nitpick.styles] include = [ "../styles/pytest.toml", "../styles/black.toml", ] """, "styles/pytest": """ ["pyproject.toml".tool.pytest] some-option = 123 """, "styles/black": """ ["pyproject.toml".tool.black] line-length = 99 missing = "value" """, } for filename, body in mapping.items(): responses.add(responses.GET, f"{base_url}/{filename}.toml", dedent(body), status=200) project = ProjectMock(tmp_path) common_pyproject = """ [tool.black] line-length = 99 [tool.pytest] some-option = 123 """ # Use full path on initial styles project.pyproject_toml(f""" [tool.nitpick] style = ["{base_url}/main"] {common_pyproject} """).api_check().assert_violations( Fuss( False, PYPROJECT_TOML, 318, " has missing values:", """ [tool.black] missing = "value" """, ))
def test_missing_different_values_any_toml(tmp_path): """Test different and missing keys/values on any TOML.""" filename = "any.toml" ProjectMock(tmp_path).save_file( filename, """ [section] # Line comment key = "original value" """, ).style(f""" ["{filename}".section] key = "new value" number = 5 """).api_check_then_fix( Fuss( True, filename, TomlPlugin.violation_base_code + SharedViolations.DIFFERENT_VALUES.code, " has different values. Use this:", """ [section] key = "new value" """, ), Fuss( True, filename, TomlPlugin.violation_base_code + SharedViolations.MISSING_VALUES.code, " has missing values:", """ [section] number = 5 """, ), ).assert_file_contents( filename, """ [section] # Line comment key = "new value" number = 5 """, )
def test_missing_different_values_pyproject_toml(tmp_path): """Test missing and different values on pyproject.toml.""" ProjectMock(tmp_path).style(""" ["pyproject.toml".something] yada = "after" ["pyproject.toml".tool] missing = "value" """).pyproject_toml(""" [something] x = 1 # comment for x yada = "before" # comment for yada yada abc = "123" # comment for abc """).api_check_then_fix( Fuss( True, PYPROJECT_TOML, 319, " has different values. Use this:", """ [something] yada = "after" """, ), Fuss( True, PYPROJECT_TOML, 318, " has missing values:", """ [tool] missing = "value" """, ), ).assert_file_contents( PYPROJECT_TOML, """ [something] x = 1 # comment for x yada = "after" # comment for yada yada abc = "123" # comment for abc [tool] missing = "value" """, )
def test_pre_commit_referenced_in_style(tmp_path): """Only check files if they have configured styles.""" ProjectMock(tmp_path).style(""" [".pre-commit-config.yaml"] fail_fast = true """).pre_commit("").api_check_then_fix( Fuss(True, PRE_COMMIT_CONFIG_YAML, 368, " has missing values:", "fail_fast: true"))
def test_missing_different_values_with_contains_json_with_contains_keys(tmp_path, datadir): """Test missing and different values with "contains_json" and "contains_keys".""" expected_package_json = (datadir / "2-expected-package.json").read_text() ProjectMock(tmp_path).named_style("package-json", datadir / "package-json-style.toml").pyproject_toml( """ [tool.nitpick] style = ["package-json"] """ ).save_file(PACKAGE_JSON, datadir / "2-actual-package.json").api_check_then_fix( Fuss( True, PACKAGE_JSON, SharedViolations.MISSING_VALUES.code + JsonPlugin.violation_base_code, " has missing values:", """ { "release": { "plugins": "<some value here>" }, "repository": { "type": "<some value here>", "url": "<some value here>" } } """, ), Fuss( True, PACKAGE_JSON, 348, " has missing values:", """ { "commitlint": { "extends": [ "@commitlint/config-conventional" ] } } """, ), ).assert_file_contents( PACKAGE_JSON, expected_package_json ).api_check_then_fix()
def test_pyproject_toml_file_present(tmp_path): """Suggest poetry init when pyproject.toml does not exist.""" ProjectMock(tmp_path, pyproject_toml=False).style(""" [nitpick.files.present] "pyproject.toml" = "Do something" """).api_check_then_fix( Fuss(False, PYPROJECT_TOML, 103, " should exist: Do something")).cli_run( f"{PYPROJECT_TOML}:1: NIP103 should exist: Do something", violations=1)
def test_missing_repos(tmp_path): """Test missing repos on file.""" ProjectMock(tmp_path).style(""" [".pre-commit-config.yaml"] fail_fast = true """).pre_commit(""" grepos: - hooks: - id: whatever """).api_check_then_fix( Fuss(True, PRE_COMMIT_CONFIG_YAML, 368, " has missing values:", "fail_fast: true"))