Home | History | Annotate | Line # | Download | only in analyzer
      1  1.1  joerg import json
      2  1.1  joerg import os
      3  1.1  joerg 
      4  1.1  joerg from enum import auto, Enum
      5  1.1  joerg from typing import Any, Dict, List, NamedTuple, Optional, Tuple
      6  1.1  joerg 
      7  1.1  joerg 
      8  1.1  joerg JSON = Dict[str, Any]
      9  1.1  joerg 
     10  1.1  joerg 
     11  1.1  joerg DEFAULT_MAP_FILE = "projects.json"
     12  1.1  joerg 
     13  1.1  joerg 
     14  1.1  joerg class DownloadType(str, Enum):
     15  1.1  joerg     GIT = "git"
     16  1.1  joerg     ZIP = "zip"
     17  1.1  joerg     SCRIPT = "script"
     18  1.1  joerg 
     19  1.1  joerg 
     20  1.1  joerg class Size(int, Enum):
     21  1.1  joerg     """
     22  1.1  joerg     Size of the project.
     23  1.1  joerg 
     24  1.1  joerg     Sizes do not directly correspond to the number of lines or files in the
     25  1.1  joerg     project.  The key factor that is important for the developers of the
     26  1.1  joerg     analyzer is the time it takes to analyze the project.  Here is how
     27  1.1  joerg     the following sizes map to times:
     28  1.1  joerg 
     29  1.1  joerg     TINY:  <1min
     30  1.1  joerg     SMALL: 1min-10min
     31  1.1  joerg     BIG:   10min-1h
     32  1.1  joerg     HUGE:  >1h
     33  1.1  joerg 
     34  1.1  joerg     The borders are a bit of a blur, especially because analysis time varies
     35  1.1  joerg     from one machine to another.  However, the relative times will stay pretty
     36  1.1  joerg     similar, and these groupings will still be helpful.
     37  1.1  joerg 
     38  1.1  joerg     UNSPECIFIED is a very special case, which is intentionally last in the list
     39  1.1  joerg     of possible sizes.  If the user wants to filter projects by one of the
     40  1.1  joerg     possible sizes, we want projects with UNSPECIFIED size to be filtered out
     41  1.1  joerg     for any given size.
     42  1.1  joerg     """
     43  1.1  joerg     TINY = auto()
     44  1.1  joerg     SMALL = auto()
     45  1.1  joerg     BIG = auto()
     46  1.1  joerg     HUGE = auto()
     47  1.1  joerg     UNSPECIFIED = auto()
     48  1.1  joerg 
     49  1.1  joerg     @staticmethod
     50  1.1  joerg     def from_str(raw_size: Optional[str]) -> "Size":
     51  1.1  joerg         """
     52  1.1  joerg         Construct a Size object from an optional string.
     53  1.1  joerg 
     54  1.1  joerg         :param raw_size: optional string representation of the desired Size
     55  1.1  joerg                          object.  None will produce UNSPECIFIED size.
     56  1.1  joerg 
     57  1.1  joerg         This method is case-insensitive, so raw sizes 'tiny', 'TINY', and
     58  1.1  joerg         'TiNy' will produce the same result.
     59  1.1  joerg         """
     60  1.1  joerg         if raw_size is None:
     61  1.1  joerg             return Size.UNSPECIFIED
     62  1.1  joerg 
     63  1.1  joerg         raw_size_upper = raw_size.upper()
     64  1.1  joerg         # The implementation is decoupled from the actual values of the enum,
     65  1.1  joerg         # so we can easily add or modify it without bothering about this
     66  1.1  joerg         # function.
     67  1.1  joerg         for possible_size in Size:
     68  1.1  joerg             if possible_size.name == raw_size_upper:
     69  1.1  joerg                 return possible_size
     70  1.1  joerg 
     71  1.1  joerg         possible_sizes = [size.name.lower() for size in Size
     72  1.1  joerg                           # no need in showing our users this size
     73  1.1  joerg                           if size != Size.UNSPECIFIED]
     74  1.1  joerg         raise ValueError(f"Incorrect project size '{raw_size}'. "
     75  1.1  joerg                          f"Available sizes are {possible_sizes}")
     76  1.1  joerg 
     77  1.1  joerg 
     78  1.1  joerg class ProjectInfo(NamedTuple):
     79  1.1  joerg     """
     80  1.1  joerg     Information about a project to analyze.
     81  1.1  joerg     """
     82  1.1  joerg     name: str
     83  1.1  joerg     mode: int
     84  1.1  joerg     source: DownloadType = DownloadType.SCRIPT
     85  1.1  joerg     origin: str = ""
     86  1.1  joerg     commit: str = ""
     87  1.1  joerg     enabled: bool = True
     88  1.1  joerg     size: Size = Size.UNSPECIFIED
     89  1.1  joerg 
     90  1.1  joerg     def with_fields(self, **kwargs) -> "ProjectInfo":
     91  1.1  joerg         """
     92  1.1  joerg         Create a copy of this project info with customized fields.
     93  1.1  joerg         NamedTuple is immutable and this is a way to create modified copies.
     94  1.1  joerg 
     95  1.1  joerg           info.enabled = True
     96  1.1  joerg           info.mode = 1
     97  1.1  joerg 
     98  1.1  joerg         can be done as follows:
     99  1.1  joerg 
    100  1.1  joerg           modified = info.with_fields(enbled=True, mode=1)
    101  1.1  joerg         """
    102  1.1  joerg         return ProjectInfo(**{**self._asdict(), **kwargs})
    103  1.1  joerg 
    104  1.1  joerg 
    105  1.1  joerg class ProjectMap:
    106  1.1  joerg     """
    107  1.1  joerg     Project map stores info about all the "registered" projects.
    108  1.1  joerg     """
    109  1.1  joerg     def __init__(self, path: Optional[str] = None, should_exist: bool = True):
    110  1.1  joerg         """
    111  1.1  joerg         :param path: optional path to a project JSON file, when None defaults
    112  1.1  joerg                      to DEFAULT_MAP_FILE.
    113  1.1  joerg         :param should_exist: flag to tell if it's an exceptional situation when
    114  1.1  joerg                              the project file doesn't exist, creates an empty
    115  1.1  joerg                              project list instead if we are not expecting it to
    116  1.1  joerg                              exist.
    117  1.1  joerg         """
    118  1.1  joerg         if path is None:
    119  1.1  joerg             path = os.path.join(os.path.abspath(os.curdir), DEFAULT_MAP_FILE)
    120  1.1  joerg 
    121  1.1  joerg         if not os.path.exists(path):
    122  1.1  joerg             if should_exist:
    123  1.1  joerg                 raise ValueError(
    124  1.1  joerg                     f"Cannot find the project map file {path}"
    125  1.1  joerg                     f"\nRunning script for the wrong directory?\n")
    126  1.1  joerg             else:
    127  1.1  joerg                 self._create_empty(path)
    128  1.1  joerg 
    129  1.1  joerg         self.path = path
    130  1.1  joerg         self._load_projects()
    131  1.1  joerg 
    132  1.1  joerg     def save(self):
    133  1.1  joerg         """
    134  1.1  joerg         Save project map back to its original file.
    135  1.1  joerg         """
    136  1.1  joerg         self._save(self.projects, self.path)
    137  1.1  joerg 
    138  1.1  joerg     def _load_projects(self):
    139  1.1  joerg         with open(self.path) as raw_data:
    140  1.1  joerg             raw_projects = json.load(raw_data)
    141  1.1  joerg 
    142  1.1  joerg             if not isinstance(raw_projects, list):
    143  1.1  joerg                 raise ValueError(
    144  1.1  joerg                     "Project map should be a list of JSON objects")
    145  1.1  joerg 
    146  1.1  joerg             self.projects = self._parse(raw_projects)
    147  1.1  joerg 
    148  1.1  joerg     @staticmethod
    149  1.1  joerg     def _parse(raw_projects: List[JSON]) -> List[ProjectInfo]:
    150  1.1  joerg         return [ProjectMap._parse_project(raw_project)
    151  1.1  joerg                 for raw_project in raw_projects]
    152  1.1  joerg 
    153  1.1  joerg     @staticmethod
    154  1.1  joerg     def _parse_project(raw_project: JSON) -> ProjectInfo:
    155  1.1  joerg         try:
    156  1.1  joerg             name: str = raw_project["name"]
    157  1.1  joerg             build_mode: int = raw_project["mode"]
    158  1.1  joerg             enabled: bool = raw_project.get("enabled", True)
    159  1.1  joerg             source: DownloadType = raw_project.get("source", "zip")
    160  1.1  joerg             size = Size.from_str(raw_project.get("size", None))
    161  1.1  joerg 
    162  1.1  joerg             if source == DownloadType.GIT:
    163  1.1  joerg                 origin, commit = ProjectMap._get_git_params(raw_project)
    164  1.1  joerg             else:
    165  1.1  joerg                 origin, commit = "", ""
    166  1.1  joerg 
    167  1.1  joerg             return ProjectInfo(name, build_mode, source, origin, commit,
    168  1.1  joerg                                enabled, size)
    169  1.1  joerg 
    170  1.1  joerg         except KeyError as e:
    171  1.1  joerg             raise ValueError(
    172  1.1  joerg                 f"Project info is required to have a '{e.args[0]}' field")
    173  1.1  joerg 
    174  1.1  joerg     @staticmethod
    175  1.1  joerg     def _get_git_params(raw_project: JSON) -> Tuple[str, str]:
    176  1.1  joerg         try:
    177  1.1  joerg             return raw_project["origin"], raw_project["commit"]
    178  1.1  joerg         except KeyError as e:
    179  1.1  joerg             raise ValueError(
    180  1.1  joerg                 f"Profect info is required to have a '{e.args[0]}' field "
    181  1.1  joerg                 f"if it has a 'git' source")
    182  1.1  joerg 
    183  1.1  joerg     @staticmethod
    184  1.1  joerg     def _create_empty(path: str):
    185  1.1  joerg         ProjectMap._save([], path)
    186  1.1  joerg 
    187  1.1  joerg     @staticmethod
    188  1.1  joerg     def _save(projects: List[ProjectInfo], path: str):
    189  1.1  joerg         with open(path, "w") as output:
    190  1.1  joerg             json.dump(ProjectMap._convert_infos_to_dicts(projects),
    191  1.1  joerg                       output, indent=2)
    192  1.1  joerg 
    193  1.1  joerg     @staticmethod
    194  1.1  joerg     def _convert_infos_to_dicts(projects: List[ProjectInfo]) -> List[JSON]:
    195  1.1  joerg         return [ProjectMap._convert_info_to_dict(project)
    196  1.1  joerg                 for project in projects]
    197  1.1  joerg 
    198  1.1  joerg     @staticmethod
    199  1.1  joerg     def _convert_info_to_dict(project: ProjectInfo) -> JSON:
    200  1.1  joerg         whole_dict = project._asdict()
    201  1.1  joerg         defaults = project._field_defaults
    202  1.1  joerg 
    203  1.1  joerg         # there is no need in serializing fields with default values
    204  1.1  joerg         for field, default_value in defaults.items():
    205  1.1  joerg             if whole_dict[field] == default_value:
    206  1.1  joerg                 del whole_dict[field]
    207  1.1  joerg 
    208  1.1  joerg         return whole_dict
    209