sqlfluff / sqlfluff
Showing 2 of 3 files from the diff.

@@ -50,20 +50,6 @@
Loading
50 50
from sqlfluff.core.plugin.host import get_plugin_manager
51 51
52 52
53 -
class RedWarningsFilter(logging.Filter):
54 -
    """This filter makes all warnings or above red."""
55 -
56 -
    def __init__(self, formatter: OutputStreamFormatter):
57 -
        super().__init__()
58 -
        self.formatter = formatter
59 -
60 -
    def filter(self, record: logging.LogRecord) -> bool:
61 -
        """Filter any warnings (or above) to turn them red."""
62 -
        if record.levelno >= logging.WARNING:
63 -
            record.msg = f"{self.formatter.colorize(record.msg, Color.red)} "
64 -
        return True
65 -
66 -
67 53
class StreamHandlerTqdm(logging.StreamHandler):
68 54
    """Modified StreamHandler which takes care of writing within `tqdm` context.
69 55
@@ -113,8 +99,16 @@
Loading
113 99
    # NB: the unicode character at the beginning is to squash any badly
114 100
    # tamed ANSI colour statements, and return us to normality.
115 101
    handler.setFormatter(logging.Formatter("\u001b[0m%(levelname)-10s %(message)s"))
102 +
116 103
    # Set up a handler to colour warnings red.
117 -
    handler.addFilter(RedWarningsFilter(formatter))
104 +
    # See: https://docs.python.org/3/library/logging.html#filter-objects
105 +
    def red_log_filter(record: logging.LogRecord) -> bool:
106 +
        if record.levelno >= logging.WARNING:
107 +
            record.msg = f"{formatter.colorize(record.msg, Color.red)} "
108 +
        return True
109 +
110 +
    handler.addFilter(red_log_filter)
111 +
118 112
    if logger:
119 113
        focus_logger = logging.getLogger(f"sqlfluff.{logger}")
120 114
        focus_logger.addHandler(handler)
@@ -313,7 +307,8 @@
Loading
313 307
    f = click.option(
314 308
        "--logger",
315 309
        type=click.Choice(
316 -
            ["templater", "lexer", "parser", "linter", "rules"], case_sensitive=False
310 +
            ["templater", "lexer", "parser", "linter", "rules", "config"],
311 +
            case_sensitive=False,
317 312
        ),
318 313
        help="Choose to limit the logging to one of the loggers.",
319 314
    )(f)

@@ -4,10 +4,11 @@
Loading
4 4
import os
5 5
import os.path
6 6
import configparser
7 +
from dataclasses import dataclass
7 8
8 9
import pluggy
9 10
from itertools import chain
10 -
from typing import Dict, Iterator, List, Tuple, Any, Optional, Union, Iterable
11 +
from typing import Dict, Iterator, List, Tuple, Any, Optional, Union, Iterable, Callable
11 12
from pathlib import Path
12 13
from sqlfluff.core.plugin.host import get_plugin_manager
13 14
from sqlfluff.core.errors import SQLFluffUserError
@@ -16,7 +17,7 @@
Loading
16 17
17 18
import toml
18 19
19 -
# Instantiate the templater logger
20 +
# Instantiate the config logger
20 21
config_logger = logging.getLogger("sqlfluff.config")
21 22
22 23
global_loader = None
@@ -28,19 +29,41 @@
Loading
28 29
29 30
ConfigElemType = Tuple[Tuple[str, ...], Any]
30 31
31 -
REMOVED_CONFIGS: Dict[Tuple[str, ...], Any] = {
32 -
    ("rules", "L007", "operator_new_lines"): (
33 -
        "Use the line_position config in the appropriate "
34 -
        "sqlfluff:layout section (e.g. sqlfluff:layout:type"
35 -
        ":binary_operator)."
32 +
33 +
@dataclass
34 +
class _RemovedConfig:
35 +
    old_path: Tuple[str, ...]
36 +
    warning: str
37 +
    new_path: Optional[Tuple[str, ...]] = None
38 +
    translation_func: Optional[Callable[[str], str]] = None
39 +
40 +
41 +
REMOVED_CONFIGS = [
42 +
    _RemovedConfig(
43 +
        ("rules", "L007", "operator_new_lines"),
44 +
        (
45 +
            "Use the line_position config in the appropriate "
46 +
            "sqlfluff:layout section (e.g. sqlfluff:layout:type"
47 +
            ":binary_operator)."
48 +
        ),
49 +
        ("layout", "type", "binary_operator", "line_position"),
50 +
        (lambda x: "trailing" if x == "after" else "leading"),
51 +
    ),
52 +
    _RemovedConfig(
53 +
        ("rules", "L019", "comma_style"),
54 +
        (
55 +
            "Use the line_position config in the appropriate "
56 +
            "sqlfluff:layout section (e.g. sqlfluff:layout:type"
57 +
            ":comma)."
58 +
        ),
59 +
        ("layout", "type", "comma", "line_position"),
60 +
        (lambda x: x),
36 61
    ),
37 -
    ("rules", "L019", "comma_style"): (
38 -
        "Use the line_position config in the appropriate "
39 -
        "sqlfluff:layout section (e.g. sqlfluff:layout:type"
40 -
        ":comma)."
62 +
    _RemovedConfig(
63 +
        ("rules", "L003", "lint_templated_tokens"),
64 +
        "No longer used.",
41 65
    ),
42 -
    ("rules", "L003", "lint_templated_tokens"): ("No longer used."),
43 -
}
66 +
]
44 67
45 68
46 69
def coerce_value(val: str) -> Any:
@@ -81,6 +104,13 @@
Loading
81 104
    Returns:
82 105
        `dict`: A combined dictionary from the input dictionaries.
83 106
107 +
    A simple example:
108 +
    >>> nested_combine({"a": {"b": "c"}}, {"a": {"d": "e"}})
109 +
    {'a': {'b': 'c', 'd': 'e'}}
110 +
111 +
    Keys overwrite left to right:
112 +
    >>> nested_combine({"a": {"b": "c"}}, {"a": {"b": "e"}})
113 +
    {'a': {'b': 'e'}}
84 114
    """
85 115
    r: dict = {}
86 116
    for d in dicts:
@@ -115,10 +145,20 @@
Loading
115 145
        left (:obj:`dict`): The object containing the *new* elements
116 146
            which will be compared against the other.
117 147
        right (:obj:`dict`): The object to compare against.
148 +
        ignore (:obj:`list` of `str`, optional): Keys to ignore.
118 149
119 150
    Returns:
120 151
        `dict`: A dictionary representing the difference.
121 152
153 +
    Basic functionality shown, especially returning the left as:
154 +
    >>> dict_diff({"a": "b", "c": "d"}, {"a": "b", "c": "e"})
155 +
    {'c': 'd'}
156 +
157 +
    Ignoring works on a key basis:
158 +
    >>> dict_diff({"a": "b"}, {"a": "c"})
159 +
    {'a': 'b'}
160 +
    >>> dict_diff({"a": "b"}, {"a": "c"}, ["a"])
161 +
    {}
122 162
    """
123 163
    buff: dict = {}
124 164
    for k in left:
@@ -183,7 +223,15 @@
Loading
183 223
184 224
    @classmethod
185 225
    def _iter_config_elems_from_dict(cls, configs: dict) -> Iterator[ConfigElemType]:
186 -
        """Walk a config dict and get config elements."""
226 +
        """Walk a config dict and get config elements.
227 +
228 +
        >>> list(
229 +
        ...    ConfigLoader._iter_config_elems_from_dict(
230 +
        ...        {"foo":{"bar":{"baz": "a", "biz": "b"}}}
231 +
        ...    )
232 +
        ... )
233 +
        [(('foo', 'bar', 'baz'), 'a'), (('foo', 'bar', 'biz'), 'b')]
234 +
        """
187 235
        for key, val in configs.items():
188 236
            if isinstance(val, dict):
189 237
                for partial_key, sub_val in cls._iter_config_elems_from_dict(val):
@@ -191,6 +239,25 @@
Loading
191 239
            else:
192 240
                yield (key,), val
193 241
242 +
    @classmethod
243 +
    def _config_elems_to_dict(cls, configs: Iterable[ConfigElemType]) -> dict:
244 +
        """Reconstruct config elements into a dict.
245 +
246 +
        >>> ConfigLoader._config_elems_to_dict(
247 +
        ...     [(("foo", "bar", "baz"), "a"), (("foo", "bar", "biz"), "b")]
248 +
        ... )
249 +
        {'foo': {'bar': {'baz': 'a', 'biz': 'b'}}}
250 +
        """
251 +
        result: Dict[str, Union[dict, str]] = {}
252 +
        for key, val in configs:
253 +
            ref = result
254 +
            for step in key[:-1]:
255 +
                if step not in ref:
256 +
                    ref[step] = {}
257 +
                ref = ref[step]  # type: ignore
258 +
            ref[key[-1]] = val
259 +
        return result
260 +
194 261
    @classmethod
195 262
    def _get_config_elems_from_toml(cls, fpath: str) -> List[ConfigElemType]:
196 263
        """Load a config from a TOML file and return a list of tuples.
@@ -271,7 +338,13 @@
Loading
271 338
272 339
    @staticmethod
273 340
    def _incorporate_vals(ctx: dict, vals: List[ConfigElemType]) -> dict:
274 -
        """Take a list of tuples and incorporate it into a dictionary."""
341 +
        """Take a list of tuples and incorporate it into a dictionary.
342 +
343 +
        >>> ConfigLoader._incorporate_vals({}, [(("a", "b"), "c")])
344 +
        {'a': {'b': 'c'}}
345 +
        >>> ConfigLoader._incorporate_vals({"a": {"b": "c"}}, [(("a", "d"), "e")])
346 +
        {'a': {'b': 'c', 'd': 'e'}}
347 +
        """
275 348
        for k, v in vals:
276 349
            # Keep a ref we can use for recursion
277 350
            r = ctx
@@ -294,17 +367,46 @@
Loading
294 367
        return ctx
295 368
296 369
    @staticmethod
297 -
    def _validate_configs(configs: Iterable[ConfigElemType], file_path):
370 +
    def _validate_configs(
371 +
        configs: Iterable[ConfigElemType], file_path
372 +
    ) -> List[ConfigElemType]:
298 373
        """Validate config elements against removed list."""
299 -
        for k, _ in configs:
300 -
            if k in REMOVED_CONFIGS:
374 +
        config_map = {cfg.old_path: cfg for cfg in REMOVED_CONFIGS}
375 +
        new_configs = []
376 +
        for k, v in configs:
377 +
            if k in config_map.keys():
301 378
                formatted_key = ":".join(k)
302 -
                raise SQLFluffUserError(
303 -
                    f"Config file {file_path} set an outdated config "
304 -
                    f"value {formatted_key}.\n\n{REMOVED_CONFIGS[k]}\n\n"
305 -
                    "See https://docs.sqlfluff.com/en/stable/configuration.html"
306 -
                    " for more details."
307 -
                )
379 +
                removed_option = config_map[k]
380 +
                # Is there a mapping option?
381 +
                if removed_option.translation_func and removed_option.new_path:
382 +
                    # Mutate and warn.
383 +
                    v = removed_option.translation_func(v)
384 +
                    k = removed_option.new_path
385 +
                    formatted_new_key = ":".join(k)
386 +
                    # NOTE: At the stage of emitting this warning, we may not yet
387 +
                    # have set up red logging because we haven't yet loaded the config
388 +
                    # file. For that reason, this error message has a bit more padding.
389 +
                    config_logger.warning(
390 +
                        f"\nWARNING: Config file {file_path} set a deprecated config "
391 +
                        f"value `{formatted_key}`. This will be removed in a later "
392 +
                        "release. This has been mapped to "
393 +
                        f"`{formatted_new_key}` set to a value of `{v}` for this run. "
394 +
                        "Please update your configuration to remove this warning. "
395 +
                        f"\n\n{removed_option.warning}\n\n"
396 +
                        "See https://docs.sqlfluff.com/en/stable/configuration.html"
397 +
                        " for more details.\n"
398 +
                    )
399 +
                else:
400 +
                    # Raise an error.
401 +
                    raise SQLFluffUserError(
402 +
                        f"Config file {file_path} set an outdated config "
403 +
                        f"value {formatted_key}.\n\n{removed_option.warning}\n\n"
404 +
                        "See https://docs.sqlfluff.com/en/stable/configuration.html"
405 +
                        " for more details."
406 +
                    )
407 +
408 +
            new_configs.append((k, v))
409 +
        return new_configs
308 410
309 411
    def load_config_file(
310 412
        self, file_dir: str, file_name: str, configs: Optional[dict] = None
@@ -315,7 +417,7 @@
Loading
315 417
            elems = self._get_config_elems_from_toml(file_path)
316 418
        else:
317 419
            elems = self._get_config_elems_from_file(file_path)
318 -
        self._validate_configs(elems, file_path)
420 +
        elems = self._validate_configs(elems, file_path)
319 421
        return self._incorporate_vals(configs or {}, elems)
320 422
321 423
    def load_config_at_path(self, path: str) -> dict:
@@ -507,13 +609,15 @@
Loading
507 609
        )
508 610
        # If overrides are provided, validate them early.
509 611
        if overrides:
510 -
            ConfigLoader._validate_configs(
511 -
                [
512 -
                    (("core",) + k, v)
513 -
                    for k, v in ConfigLoader._iter_config_elems_from_dict(overrides)
514 -
                ],
515 -
                "<provided overrides>",
516 -
            )
612 +
            overrides = ConfigLoader._config_elems_to_dict(
613 +
                ConfigLoader._validate_configs(
614 +
                    [
615 +
                        (("core",) + k, v)
616 +
                        for k, v in ConfigLoader._iter_config_elems_from_dict(overrides)
617 +
                    ],
618 +
                    "<provided overrides>",
619 +
                )
620 +
            )["core"]
517 621
        self._overrides = overrides  # We only store this for child configs
518 622
519 623
        # Fetch a fresh plugin manager if we weren't provided with one
@@ -522,8 +626,11 @@
Loading
522 626
        defaults = nested_combine(*self._plugin_manager.hook.load_default_config())
523 627
        # If any existing configs are provided. Validate them:
524 628
        if configs:
525 -
            ConfigLoader._validate_configs(
526 -
                ConfigLoader._iter_config_elems_from_dict(configs), "<provided configs>"
629 +
            configs = ConfigLoader._config_elems_to_dict(
630 +
                ConfigLoader._validate_configs(
631 +
                    ConfigLoader._iter_config_elems_from_dict(configs),
632 +
                    "<provided configs>",
633 +
                )
527 634
            )
528 635
        self._configs = nested_combine(
529 636
            defaults, configs or {"core": {}}, {"core": overrides or {}}
Files Coverage
src/sqlfluff 100.00%
Project Totals (185 files) 100.00%
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading