brintos

brintos / llvm-project-archived public Read only

0
0
Text · 6.6 KiB · 64f819f Raw
213 lines · python
1import json2import os3 4from enum import auto, Enum5from typing import Any, Dict, List, NamedTuple, Optional, Tuple6 7 8JSON = Dict[str, Any]9 10 11DEFAULT_MAP_FILE = "projects.json"12 13 14class DownloadType(str, Enum):15    GIT = "git"16    ZIP = "zip"17    SCRIPT = "script"18 19 20class Size(int, Enum):21    """22    Size of the project.23 24    Sizes do not directly correspond to the number of lines or files in the25    project.  The key factor that is important for the developers of the26    analyzer is the time it takes to analyze the project.  Here is how27    the following sizes map to times:28 29    TINY:  <1min30    SMALL: 1min-10min31    BIG:   10min-1h32    HUGE:  >1h33 34    The borders are a bit of a blur, especially because analysis time varies35    from one machine to another.  However, the relative times will stay pretty36    similar, and these groupings will still be helpful.37 38    UNSPECIFIED is a very special case, which is intentionally last in the list39    of possible sizes.  If the user wants to filter projects by one of the40    possible sizes, we want projects with UNSPECIFIED size to be filtered out41    for any given size.42    """43 44    TINY = auto()45    SMALL = auto()46    BIG = auto()47    HUGE = auto()48    UNSPECIFIED = auto()49 50    @staticmethod51    def from_str(raw_size: Optional[str]) -> "Size":52        """53        Construct a Size object from an optional string.54 55        :param raw_size: optional string representation of the desired Size56                         object.  None will produce UNSPECIFIED size.57 58        This method is case-insensitive, so raw sizes 'tiny', 'TINY', and59        'TiNy' will produce the same result.60        """61        if raw_size is None:62            return Size.UNSPECIFIED63 64        raw_size_upper = raw_size.upper()65        # The implementation is decoupled from the actual values of the enum,66        # so we can easily add or modify it without bothering about this67        # function.68        for possible_size in Size:69            if possible_size.name == raw_size_upper:70                return possible_size71 72        possible_sizes = [73            size.name.lower()74            for size in Size75            # no need in showing our users this size76            if size != Size.UNSPECIFIED77        ]78        raise ValueError(79            f"Incorrect project size '{raw_size}'. "80            f"Available sizes are {possible_sizes}"81        )82 83 84class ProjectInfo(NamedTuple):85    """86    Information about a project to analyze.87    """88 89    name: str90    mode: int91    source: DownloadType = DownloadType.SCRIPT92    origin: str = ""93    commit: str = ""94    enabled: bool = True95    size: Size = Size.UNSPECIFIED96 97    def with_fields(self, **kwargs) -> "ProjectInfo":98        """99        Create a copy of this project info with customized fields.100        NamedTuple is immutable and this is a way to create modified copies.101 102          info.enabled = True103          info.mode = 1104 105        can be done as follows:106 107          modified = info.with_fields(enbled=True, mode=1)108        """109        return ProjectInfo(**{**self._asdict(), **kwargs})110 111 112class ProjectMap:113    """114    Project map stores info about all the "registered" projects.115    """116 117    def __init__(self, path: Optional[str] = None, should_exist: bool = True):118        """119        :param path: optional path to a project JSON file, when None defaults120                     to DEFAULT_MAP_FILE.121        :param should_exist: flag to tell if it's an exceptional situation when122                             the project file doesn't exist, creates an empty123                             project list instead if we are not expecting it to124                             exist.125        """126        if path is None:127            path = os.path.join(os.path.abspath(os.curdir), DEFAULT_MAP_FILE)128 129        if not os.path.exists(path):130            if should_exist:131                raise ValueError(132                    f"Cannot find the project map file {path}"133                    f"\nRunning script for the wrong directory?\n"134                )135            else:136                self._create_empty(path)137 138        self.path = path139        self._load_projects()140 141    def save(self):142        """143        Save project map back to its original file.144        """145        self._save(self.projects, self.path)146 147    def _load_projects(self):148        with open(self.path) as raw_data:149            raw_projects = json.load(raw_data)150 151            if not isinstance(raw_projects, list):152                raise ValueError("Project map should be a list of JSON objects")153 154            self.projects = self._parse(raw_projects)155 156    @staticmethod157    def _parse(raw_projects: List[JSON]) -> List[ProjectInfo]:158        return [ProjectMap._parse_project(raw_project) for raw_project in raw_projects]159 160    @staticmethod161    def _parse_project(raw_project: JSON) -> ProjectInfo:162        try:163            name: str = raw_project["name"]164            build_mode: int = raw_project["mode"]165            enabled: bool = raw_project.get("enabled", True)166            source: DownloadType = raw_project.get("source", "zip")167            size = Size.from_str(raw_project.get("size", None))168 169            if source == DownloadType.GIT:170                origin, commit = ProjectMap._get_git_params(raw_project)171            else:172                origin, commit = "", ""173 174            return ProjectInfo(name, build_mode, source, origin, commit, enabled, size)175 176        except KeyError as e:177            raise ValueError(f"Project info is required to have a '{e.args[0]}' field")178 179    @staticmethod180    def _get_git_params(raw_project: JSON) -> Tuple[str, str]:181        try:182            return raw_project["origin"], raw_project["commit"]183        except KeyError as e:184            raise ValueError(185                f"Profect info is required to have a '{e.args[0]}' field "186                f"if it has a 'git' source"187            )188 189    @staticmethod190    def _create_empty(path: str):191        ProjectMap._save([], path)192 193    @staticmethod194    def _save(projects: List[ProjectInfo], path: str):195        with open(path, "w") as output:196            json.dump(ProjectMap._convert_infos_to_dicts(projects), output, indent=2)197 198    @staticmethod199    def _convert_infos_to_dicts(projects: List[ProjectInfo]) -> List[JSON]:200        return [ProjectMap._convert_info_to_dict(project) for project in projects]201 202    @staticmethod203    def _convert_info_to_dict(project: ProjectInfo) -> JSON:204        whole_dict = project._asdict()205        defaults = project._field_defaults206 207        # there is no need in serializing fields with default values208        for field, default_value in defaults.items():209            if whole_dict[field] == default_value:210                del whole_dict[field]211 212        return whole_dict213