4 from enum
import auto
, Enum
5 from typing
import Any
, Dict
, List
, NamedTuple
, Optional
, Tuple
11 DEFAULT_MAP_FILE
= "projects.json"
14 class DownloadType(str, Enum
):
20 class Size(int, Enum
):
24 Sizes do not directly correspond to the number of lines or files in the
25 project. The key factor that is important for the developers of the
26 analyzer is the time it takes to analyze the project. Here is how
27 the following sizes map to times:
34 The borders are a bit of a blur, especially because analysis time varies
35 from one machine to another. However, the relative times will stay pretty
36 similar, and these groupings will still be helpful.
38 UNSPECIFIED is a very special case, which is intentionally last in the list
39 of possible sizes. If the user wants to filter projects by one of the
40 possible sizes, we want projects with UNSPECIFIED size to be filtered out
51 def from_str(raw_size
: Optional
[str]) -> "Size":
53 Construct a Size object from an optional string.
55 :param raw_size: optional string representation of the desired Size
56 object. None will produce UNSPECIFIED size.
58 This method is case-insensitive, so raw sizes 'tiny', 'TINY', and
59 'TiNy' will produce the same result.
62 return Size
.UNSPECIFIED
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 this
68 for possible_size
in Size
:
69 if possible_size
.name
== raw_size_upper
:
75 # no need in showing our users this size
76 if size
!= Size
.UNSPECIFIED
79 f
"Incorrect project size '{raw_size}'. "
80 f
"Available sizes are {possible_sizes}"
84 class ProjectInfo(NamedTuple
):
86 Information about a project to analyze.
91 source
: DownloadType
= DownloadType
.SCRIPT
95 size
: Size
= Size
.UNSPECIFIED
97 def with_fields(self
, **kwargs
) -> "ProjectInfo":
99 Create a copy of this project info with customized fields.
100 NamedTuple is immutable and this is a way to create modified copies.
105 can be done as follows:
107 modified = info.with_fields(enbled=True, mode=1)
109 return ProjectInfo(**{**self
._asdict
(), **kwargs
})
114 Project map stores info about all the "registered" projects.
117 def __init__(self
, path
: Optional
[str] = None, should_exist
: bool = True):
119 :param path: optional path to a project JSON file, when None defaults
121 :param should_exist: flag to tell if it's an exceptional situation when
122 the project file doesn't exist, creates an empty
123 project list instead if we are not expecting it to
127 path
= os
.path
.join(os
.path
.abspath(os
.curdir
), DEFAULT_MAP_FILE
)
129 if not os
.path
.exists(path
):
132 f
"Cannot find the project map file {path}"
133 f
"\nRunning script for the wrong directory?\n"
136 self
._create
_empty
(path
)
139 self
._load
_projects
()
143 Save project map back to its original file.
145 self
._save
(self
.projects
, self
.path
)
147 def _load_projects(self
):
148 with
open(self
.path
) as raw_data
:
149 raw_projects
= json
.load(raw_data
)
151 if not isinstance(raw_projects
, list):
152 raise ValueError("Project map should be a list of JSON objects")
154 self
.projects
= self
._parse
(raw_projects
)
157 def _parse(raw_projects
: List
[JSON
]) -> List
[ProjectInfo
]:
158 return [ProjectMap
._parse
_project
(raw_project
) for raw_project
in raw_projects
]
161 def _parse_project(raw_project
: JSON
) -> ProjectInfo
:
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))
169 if source
== DownloadType
.GIT
:
170 origin
, commit
= ProjectMap
._get
_git
_params
(raw_project
)
172 origin
, commit
= "", ""
174 return ProjectInfo(name
, build_mode
, source
, origin
, commit
, enabled
, size
)
176 except KeyError as e
:
177 raise ValueError(f
"Project info is required to have a '{e.args[0]}' field")
180 def _get_git_params(raw_project
: JSON
) -> Tuple
[str, str]:
182 return raw_project
["origin"], raw_project
["commit"]
183 except KeyError as e
:
185 f
"Profect info is required to have a '{e.args[0]}' field "
186 f
"if it has a 'git' source"
190 def _create_empty(path
: str):
191 ProjectMap
._save
([], path
)
194 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)
199 def _convert_infos_to_dicts(projects
: List
[ProjectInfo
]) -> List
[JSON
]:
200 return [ProjectMap
._convert
_info
_to
_dict
(project
) for project
in projects
]
203 def _convert_info_to_dict(project
: ProjectInfo
) -> JSON
:
204 whole_dict
= project
._asdict
()
205 defaults
= project
._field
_defaults
207 # there is no need in serializing fields with default values
208 for field
, default_value
in defaults
.items():
209 if whole_dict
[field
] == default_value
:
210 del whole_dict
[field
]