| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- import codecs
- import json
- import os
- import pkgutil
- from pathlib import Path
- from typing import Any, Dict, List, Optional, Tuple, Union
- import yaml
- from prospector.profiles.exceptions import CannotParseProfile, ProfileNotFound
- from prospector.tools import DEFAULT_TOOLS, TOOLS
- BUILTIN_PROFILE_PATH = (Path(__file__).parent / "profiles").absolute()
- class ProspectorProfile:
- def __init__(self, name: str, profile_dict: Dict[str, Any], inherit_order: List[str]):
- self.name = name
- self.inherit_order = inherit_order
- self.ignore_paths = _ensure_list(profile_dict.get("ignore-paths", []))
- # The 'ignore' directive is an old one which should be deprecated at some point
- self.ignore_patterns = _ensure_list(profile_dict.get("ignore-patterns", []) + profile_dict.get("ignore", []))
- self.output_format = profile_dict.get("output-format")
- self.output_target = profile_dict.get("output-target")
- self.autodetect = profile_dict.get("autodetect", True)
- self.uses = [
- uses for uses in _ensure_list(profile_dict.get("uses", [])) if uses in ("django", "celery", "flask")
- ]
- self.max_line_length = profile_dict.get("max-line-length")
- # informational shorthands
- self.strictness = profile_dict.get("strictness")
- self.test_warnings = profile_dict.get("test-warnings")
- self.doc_warnings = profile_dict.get("doc-warnings")
- self.member_warnings = profile_dict.get("member-warnings")
- # TODO: this is needed by Landscape but not by prospector; there is probably a better place for it
- self.requirements = _ensure_list(profile_dict.get("requirements", []))
- for tool in TOOLS:
- tool_conf = profile_dict.get(tool, {})
- # set the defaults for everything
- conf: Dict[str, Any] = {"disable": [], "enable": [], "run": None, "options": {}}
- # use the "old" tool name
- conf.update(tool_conf)
- if self.max_line_length is not None and tool in ("pylint", "pycodestyle"):
- conf["options"]["max-line-length"] = self.max_line_length
- setattr(self, tool, conf)
- def get_disabled_messages(self, tool_name):
- disable = getattr(self, tool_name)["disable"]
- enable = getattr(self, tool_name)["enable"]
- return list(set(disable) - set(enable))
- def is_tool_enabled(self, name):
- enabled = getattr(self, name).get("run")
- if enabled is not None:
- return enabled
- # this is not explicitly enabled or disabled, so use the default
- return name in DEFAULT_TOOLS
- def list_profiles(self):
- # this profile is itself included
- return [str(profile) for profile in self.inherit_order]
- def as_dict(self):
- out = {
- "ignore-paths": self.ignore_paths,
- "ignore-patterns": self.ignore_patterns,
- "output-format": self.output_format,
- "output-target": self.output_target,
- "autodetect": self.autodetect,
- "uses": self.uses,
- "max-line-length": self.max_line_length,
- "member-warnings": self.member_warnings,
- "doc-warnings": self.doc_warnings,
- "test-warnings": self.test_warnings,
- "strictness": self.strictness,
- "requirements": self.requirements,
- }
- for tool in TOOLS:
- out[tool] = getattr(self, tool)
- return out
- def as_json(self):
- return json.dumps(self.as_dict())
- def as_yaml(self):
- return yaml.safe_dump(self.as_dict())
- @staticmethod
- def load(
- name_or_path: Union[str, Path],
- profile_path: List[Path],
- allow_shorthand: bool = True,
- forced_inherits: Optional[List[str]] = None,
- ):
- # First simply load all of the profiles and those that it explicitly inherits from
- data, inherits = _load_and_merge(
- name_or_path,
- profile_path,
- allow_shorthand,
- forced_inherits=forced_inherits or [],
- )
- return ProspectorProfile(str(name_or_path), data, inherits)
- def _is_valid_extension(filename):
- ext = os.path.splitext(filename)[1]
- return ext in (".yml", ".yaml")
- def _load_content_package(name):
- name_split = name.split(":", 1)
- module_name = f"prospector_profile_{name_split[0]}"
- file_names = (
- ["prospector.yaml", "prospector.yml"]
- if len(name_split) == 1
- else [f"{name_split[1]}.yaml", f"{name_split[1]}.yaml"]
- )
- data = None
- used_name = None
- for file_name in file_names:
- used_name = f"{module_name}:{file_name}"
- data = pkgutil.get_data(module_name, file_name)
- if data is not None:
- break
- if data is None:
- return None
- try:
- return yaml.safe_load(data) or {}
- except yaml.parser.ParserError as parse_error:
- raise CannotParseProfile(used_name, parse_error) from parse_error
- def _load_content(name_or_path, profile_path):
- filename = None
- optional = False
- if isinstance(name_or_path, str) and name_or_path.endswith("?"):
- optional = True
- name_or_path = name_or_path[:-1]
- if _is_valid_extension(name_or_path):
- for path in profile_path:
- filepath = os.path.join(path, name_or_path)
- if os.path.exists(filepath):
- # this is a full path that we can load
- filename = filepath
- break
- else:
- for path in profile_path:
- for ext in ("yml", "yaml"):
- filepath = os.path.join(path, f"{name_or_path}.{ext}")
- if os.path.exists(filepath):
- filename = filepath
- break
- if filename is None:
- result = _load_content_package(name_or_path)
- if result is not None:
- return result
- if optional:
- return {}
- raise ProfileNotFound(name_or_path, profile_path)
- with codecs.open(filename) as fct:
- try:
- return yaml.safe_load(fct) or {}
- except yaml.parser.ParserError as parse_error:
- raise CannotParseProfile(filename, parse_error) from parse_error
- def _ensure_list(value):
- if isinstance(value, list):
- return value
- return [value]
- def _simple_merge_dict(priority, base):
- out = dict(base.items())
- out.update(dict(priority.items()))
- return out
- def _merge_tool_config(priority, base):
- out = dict(base.items())
- # add options that are missing, but keep existing options from the priority dictionary
- # TODO: write a unit test for this :-|
- out["options"] = _simple_merge_dict(priority.get("options", {}), base.get("options", {}))
- # copy in some basic pieces
- for key in ("run", "load-plugins"):
- value = priority.get(key, base.get(key))
- if value is not None:
- out[key] = value
- # anything enabled in the 'priority' dict is removed
- # from 'disabled' in the base dict and vice versa
- base_disabled = base.get("disable") or []
- base_enabled = base.get("enable") or []
- pri_disabled = priority.get("disable") or []
- pri_enabled = priority.get("enable") or []
- out["disable"] = list(set(pri_disabled) | (set(base_disabled) - set(pri_enabled)))
- out["enable"] = list(set(pri_enabled) | (set(base_enabled) - set(pri_disabled)))
- return out
- def _merge_profile_dict(priority: dict, base: dict) -> dict:
- # copy the base dict into our output
- out = dict(base.items())
- for key, value in priority.items():
- if key in (
- "strictness",
- "doc-warnings",
- "test-warnings",
- "member-warnings",
- "output-format",
- "autodetect",
- "max-line-length",
- "pep8",
- ):
- # some keys are simple values which are overwritten
- out[key] = value
- elif key in (
- "ignore",
- "ignore-patterns",
- "ignore-paths",
- "uses",
- "requirements",
- "python-targets",
- "output-target",
- ):
- # some keys should be appended
- out[key] = _ensure_list(value) + _ensure_list(base.get(key, []))
- elif key in TOOLS:
- # this is tool config!
- out[key] = _merge_tool_config(value, base.get(key, {}))
- return out
- def _determine_strictness(profile_dict, inherits):
- for profile in inherits:
- if profile.startswith("strictness_"):
- return None, False
- strictness = profile_dict.get("strictness")
- if strictness is None:
- return None, False
- return ("strictness_%s" % strictness), True
- def _determine_pep8(profile_dict):
- pep8 = profile_dict.get("pep8")
- if pep8 == "full":
- return "full_pep8", True
- if pep8 == "none":
- return "no_pep8", True
- if isinstance(pep8, dict) and pep8.get("full", False):
- return "full_pep8", False
- return None, False
- def _determine_doc_warnings(profile_dict):
- doc_warnings = profile_dict.get("doc-warnings")
- if doc_warnings is None:
- return None, False
- return ("doc_warnings" if doc_warnings else "no_doc_warnings"), True
- def _determine_test_warnings(profile_dict):
- test_warnings = profile_dict.get("test-warnings")
- if test_warnings is None:
- return None, False
- return (None if test_warnings else "no_test_warnings"), True
- def _determine_member_warnings(profile_dict):
- member_warnings = profile_dict.get("member-warnings")
- if member_warnings is None:
- return None, False
- return ("member_warnings" if member_warnings else "no_member_warnings"), True
- def _determine_implicit_inherits(profile_dict, already_inherits, shorthands_found):
- # Note: the ordering is very important here - the earlier items
- # in the list have precedence over the later items. The point of
- # the doc/test/pep8 profiles is usually to restore items which were
- # turned off in the strictness profile, so they must appear first.
- implicit = [
- ("pep8", _determine_pep8(profile_dict)),
- ("docs", _determine_doc_warnings(profile_dict)),
- ("tests", _determine_test_warnings(profile_dict)),
- ("strictness", _determine_strictness(profile_dict, already_inherits)),
- ("members", _determine_member_warnings(profile_dict)),
- ]
- inherits = []
- for shorthand_name, determined in implicit:
- if shorthand_name in shorthands_found:
- continue
- extra_inherits, shorthand_found = determined
- if not shorthand_found:
- continue
- shorthands_found.add(shorthand_name)
- if extra_inherits is not None:
- inherits.append(extra_inherits)
- return inherits, shorthands_found
- def _append_profiles(name, profile_path, data, inherit_list, allow_shorthand=False):
- new_data, new_il, _ = _load_profile(name, profile_path, allow_shorthand=allow_shorthand)
- data.update(new_data)
- inherit_list += new_il
- return data, inherit_list
- def _load_and_merge(
- name_or_path: Union[str, Path],
- profile_path: List[Path],
- allow_shorthand: bool = True,
- forced_inherits: List[str] = None,
- ) -> Tuple[Dict[str, Any], List[str]]:
- # First simply load all of the profiles and those that it explicitly inherits from
- data, inherit_list, shorthands_found = _load_profile(
- str(name_or_path),
- profile_path,
- allow_shorthand=allow_shorthand,
- forced_inherits=forced_inherits or [],
- )
- if allow_shorthand:
- if "docs" not in shorthands_found:
- data, inherit_list = _append_profiles("no_doc_warnings", profile_path, data, inherit_list)
- if "members" not in shorthands_found:
- data, inherit_list = _append_profiles("no_member_warnings", profile_path, data, inherit_list)
- if "tests" not in shorthands_found:
- data, inherit_list = _append_profiles("no_test_warnings", profile_path, data, inherit_list)
- if "strictness" not in shorthands_found:
- # if no strictness was specified, then we should manually insert the medium strictness
- for inherit in inherit_list:
- if inherit.startswith("strictness_"):
- break
- else:
- data, inherit_list = _append_profiles("strictness_medium", profile_path, data, inherit_list)
- # Now we merge all of the values together, from 'right to left' (ie, from the
- # top of the inheritance tree to the bottom). This means that the lower down
- # values overwrite those from above, meaning that the initially provided profile
- # has precedence.
- merged: dict = {}
- for name in inherit_list[::-1]:
- priority = data[name]
- merged = _merge_profile_dict(priority, merged)
- return merged, inherit_list
- def _transform_legacy(profile_dict):
- """
- After pep8 was renamed to pycodestyle, this pre-filter just moves profile
- config blocks using the old name to use the new name, merging if both are
- specified.
- Same for pep257->pydocstyle
- """
- out = {}
- # copy in existing pep8/pep257 using new names to start
- if "pycodestyle" in profile_dict:
- out["pycodestyle"] = profile_dict["pycodestyle"]
- if "pydocstyle" in profile_dict:
- out["pydocstyle"] = profile_dict["pydocstyle"]
- # pep8 is tricky as it's overloaded as a tool configuration and a shorthand
- # first, is this the short "pep8: full" version or a configuration of the
- # pycodestyle tool using the old name?
- if "pep8" in profile_dict:
- pep8conf = profile_dict["pep8"]
- if isinstance(pep8conf, dict):
- # merge in with existing config if there is any
- out["pycodestyle"] = _simple_merge_dict(out.get("pycodestyle", {}), pep8conf)
- else:
- # otherwise it's shortform, just copy it in directly
- out["pep8"] = pep8conf
- del profile_dict["pep8"]
- if "pep257" in profile_dict:
- out["pydocstyle"] = _simple_merge_dict(out.get("pydocstyle", {}), profile_dict["pep257"])
- del profile_dict["pep257"]
- # now just copy the rest in
- for key, value in profile_dict.items():
- if key in ("pycodestyle", "pydocstyle"):
- # already handled these
- continue
- out[key] = value
- return out
- def _load_profile(
- name_or_path,
- profile_path,
- shorthands_found=None,
- already_loaded=None,
- allow_shorthand=True,
- forced_inherits=None,
- ):
- # recursively get the contents of the basic profile and those it inherits from
- base_contents = _load_content(name_or_path, profile_path)
- base_contents = _transform_legacy(base_contents)
- inherit_order = [name_or_path]
- shorthands_found = shorthands_found or set()
- already_loaded = already_loaded or []
- already_loaded.append(name_or_path)
- inherits = _ensure_list(base_contents.get("inherits", []))
- if forced_inherits is not None:
- inherits += forced_inherits
- # There are some 'shorthand' options in profiles which implicitly mean that we
- # should inherit from some of prospector's built-in profiles
- if base_contents.get("allow-shorthand", True) and allow_shorthand:
- extra_inherits, extra_shorthands = _determine_implicit_inherits(base_contents, inherits, shorthands_found)
- inherits += extra_inherits
- shorthands_found |= extra_shorthands
- contents_dict = {name_or_path: base_contents}
- for inherit_profile in inherits:
- if inherit_profile in already_loaded:
- # we already have this loaded and in the list
- continue
- already_loaded.append(inherit_profile)
- new_cd, new_il, new_sh = _load_profile(
- inherit_profile,
- profile_path,
- shorthands_found,
- already_loaded,
- allow_shorthand,
- )
- contents_dict.update(new_cd)
- inherit_order += new_il
- shorthands_found |= new_sh
- # note: a new list is returned here rather than simply using inherit_order to give astroid a
- # clue about the type of the returned object, as otherwise it can recurse infinitely and crash,
- # this meaning that prospector does not run on prospector cleanly!
- return contents_dict, list(inherit_order), shorthands_found
|