Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds JSON Schema for Fixit configs. #188

Merged
merged 13 commits into from
May 27, 2021
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1 @@
include README.rst LICENSE CODE_OF_CONDUCT.md CONTRIBUTING.md requirements.txt requirements-dev.txt fixit/py.typed
include README.rst LICENSE CODE_OF_CONDUCT.md CONTRIBUTING.md requirements.txt requirements-dev.txt fixit/py.typed config.schema.json
lisroach marked this conversation as resolved.
Show resolved Hide resolved
39 changes: 22 additions & 17 deletions fixit/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
# LICENSE file in the root directory of this source tree.

import distutils.spawn


try:
import importlib.resources as pkg_resources
except ImportError: # For <=3.6
import importlib_resources as pkg_resources

import json
import os
import re
from dataclasses import asdict
Expand All @@ -12,12 +20,14 @@
from typing import Any, Dict, Pattern, Set

import yaml
from jsonschema import validate

from fixit.common.base import LintConfig
from fixit.common.utils import LintRuleCollectionT, import_distinct_rules_from_package


LINT_CONFIG_FILE_NAME: Path = Path(".fixit.config.yaml")
LINT_CONFIG_SCHEMA_NAME: str = "config.schema.json"

# https://gitlab.com/pycqa/flake8/blob/9631dac52aa6ed8a3de9d0983c/src/flake8/defaults.py
NOQA_INLINE_REGEXP: Pattern[str] = re.compile(
Expand Down Expand Up @@ -70,22 +80,25 @@
def get_validated_settings(
file_content: Dict[str, Any], current_dir: Path
) -> Dict[str, Any]:
try:
# __package__ should never be none (config.py should not be run directly)
# But use .get() to make pyre happy
pkg = globals().get("__package__")
assert pkg, "No package was found, config types not validated."
config = pkg_resources.read_text(pkg, "config.schema.json")
# Validates the types and presence of the keys
schema = json.loads(config)
validate(instance=file_content, schema=schema)
except (AssertionError, FileNotFoundError):
lisroach marked this conversation as resolved.
Show resolved Hide resolved
pass

settings = {}
for list_setting_name in LIST_SETTINGS:
if list_setting_name in file_content:
if not (
isinstance(file_content[list_setting_name], list)
and all(isinstance(s, str) for s in file_content[list_setting_name])
):
raise TypeError(
f"Expected list of strings for `{list_setting_name}` setting."
)
settings[list_setting_name] = file_content[list_setting_name]
for path_setting_name in PATH_SETTINGS:
if path_setting_name in file_content:
setting_value = file_content[path_setting_name]
if not isinstance(setting_value, str):
raise TypeError(f"Expected string for `{path_setting_name}` setting.")
abspath: Path = (current_dir / setting_value).resolve()
else:
abspath: Path = current_dir
Expand All @@ -95,17 +108,9 @@ def get_validated_settings(
for nested_setting_name in NESTED_SETTINGS:
if nested_setting_name in file_content:
nested_setting = file_content[nested_setting_name]
if not isinstance(nested_setting, dict):
raise TypeError(
f"Expected key-value pairs for `{nested_setting_name}` setting."
)
settings[nested_setting_name] = {}
# Verify that each setting is also a mapping
for k, v in nested_setting.items():
if not isinstance(v, dict):
raise TypeError(
f"Expected key-value pairs for `{v}` setting in {nested_setting_name}."
)
settings[nested_setting_name].update({k: v})

return settings
Expand Down
52 changes: 52 additions & 0 deletions fixit/common/config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"$id": "config.schema.json",
"title": "FixIt Configuration File Schema",
"description": "Schema definition for FixIt configuration files",
"type": "object",
"properties": {
"block_list_patterns": {
"description": "A list of patterns that indicate that a file should not be linted.",
"type": "array",
"items": {
"type": "string"
}
},
"block_list_rules": {
"description": "A list of rules (whether custom or from Fixit) that should not be applied to the repository.",
"type": "array",
"items": {
"type": "string"
}
},
"fixture_dir": {
"description": "The directory in which fixture files required for unit testing are to be found.",
"type": "string"
},
"use_noqa": {
"description": "Defaults to False. Use True to support Flake8 lint suppression comment: noqa.",
"type": "boolean"
},
"formatter": {
"description": "A list of the formatter commands to use after a lint is complete.",
"type": "array",
"items": {
"type": "string"
}
},
"packages": {
"description": "The Python packages in which to search for lint rules.",
"type": "array",
"items": {
"type": "string"
}
},
"repo_root": {
"description": "The path to the repository root.",
"type": "string"
},
"rule_config": {
"description": "Rule-specific configurations.",
"type": "object"
}
}
}
53 changes: 53 additions & 0 deletions fixit/common/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import os
from pathlib import Path

from jsonschema.exceptions import ValidationError
from libcst.testing.utils import UnitTest

from fixit.common.config import get_validated_settings


class TestConfig(UnitTest):
def test_validated_settings_with_bad_types(self) -> None:
bad_config = {"block_list_rules": False}
with self.assertRaises(ValidationError) as ex:
get_validated_settings(bad_config, Path("."))
self.assertIn("False is not of type 'array'", str(ex.exception))
lisroach marked this conversation as resolved.
Show resolved Hide resolved

def test_validated_settings_with_correct_types(self) -> None:
config = {"block_list_rules": ["FakeRule"]}
settings = get_validated_settings(config, Path("."))
self.assertEqual(
{"block_list_rules": ["FakeRule"], "fixture_dir": ".", "repo_root": "."},
settings,
)

def test_validated_settings_all_keys(self) -> None:
self.maxDiff = None
config = {
"formatter": ["black", "-", "--no-diff"],
"packages": ["python.fixit.rules"],
"block_list_rules": ["Flake8PseudoLintRule"],
"fixture_dir": f"{os.getcwd()}",
"repo_root": f"{os.getcwd()}",
"rule_config": {
"UnusedImportsRule": {
"ignored_unused_modules": [
"__future__",
"__static__",
"__static__.compiler_flags",
"__strict__",
]
},
},
}
settings = get_validated_settings(config, Path("."))
self.assertEqual(
config,
settings,
)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
flake8>=3.8.1
libcst>=0.3.18
pyyaml>=5.2
jsonschema>=3.2.0
importlib-resources>=5.1.2