diff options
author | Marc-André Lureau <marcandre.lureau@redhat.com> | 2025-09-02 12:08:14 +0400 |
---|---|---|
committer | Marc-André Lureau <marcandre.lureau@redhat.com> | 2025-09-03 16:18:12 +0400 |
commit | 84e464f19226ff2d41f4910fa66ea2ca94a3b1ed (patch) | |
tree | 4e57ac23922daf0dfd7b95a9471be11cc211c551 | |
parent | 924025f6222c59c49fa9bd9fc26a00599c887bc6 (diff) | |
download | libvirt-ci-84e464f19226ff2d41f4910fa66ea2ca94a3b1ed.zip libvirt-ci-84e464f19226ff2d41f4910fa66ea2ca94a3b1ed.tar.gz libvirt-ci-84e464f19226ff2d41f4910fa66ea2ca94a3b1ed.tar.bz2 |
projects: add type annotations
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
-rw-r--r-- | lcitool/projects.py | 109 | ||||
-rw-r--r-- | pyproject.toml | 1 |
2 files changed, 70 insertions, 40 deletions
diff --git a/lcitool/projects.py b/lcitool/projects.py index 099c07b..f1187cb 100644 --- a/lcitool/projects.py +++ b/lcitool/projects.py @@ -11,8 +11,10 @@ from urllib.parse import urlparse import yaml from lcitool import util, LcitoolError -from lcitool.packages import PyPIPackage, CPANPackage +from lcitool.packages import Package, PyPIPackage, CPANPackage from lcitool.util import DataDir +from lcitool.targets import BuildTarget +from typing import Dict, Iterator, List, Optional, Union log = logging.getLogger(__name__) @@ -25,7 +27,7 @@ class ProjectError(LcitoolError): subclass on failure. """ - def __init__(self, message): + def __init__(self, message: str): super().__init__(message, "Project") @@ -38,27 +40,29 @@ class Projects: """ @property - def public(self): + def public(self) -> Dict[str, "Project"]: if self._public is None: self._load_public() + assert self._public is not None return self._public @property - def names(self): + def names(self) -> List[str]: return list(self.public.keys()) @property - def internal(self): + def internal(self) -> Dict[str, "Project"]: if self._internal is None: self._load_internal() + assert self._internal is not None return self._internal - def __init__(self, data_dir=DataDir()): + def __init__(self, data_dir: DataDir = DataDir()): self._data_dir = data_dir - self._public = None - self._internal = None + self._public: Optional[Dict[str, "Project"]] = None + self._internal: Optional[Dict[str, "Project"]] = None - def _load_projects_from_files(self, files): + def _load_projects_from_files(self, files: Iterator[Path]) -> Dict[str, "Project"]: projects = {} for item in files: @@ -67,17 +71,17 @@ class Projects: return projects - def _load_public(self): + def _load_public(self) -> None: files = self._data_dir.list_files("facts/projects", ".yml") self._public = self._load_projects_from_files(files) - def _load_internal(self): + def _load_internal(self) -> None: files = self._data_dir.list_files( "facts/projects/internal", ".yml", internal=True ) self._internal = self._load_projects_from_files(files) - def _resolve_remote(self, name): + def _resolve_remote(self, name: str) -> str: # Pre-defined projects have no "/" if "/" not in name: return name @@ -102,35 +106,42 @@ class Projects: raise ProjectError(f"Project {name} should refer to a project YML file") projname = path.stem - if projname in self._public: - if self._public[projname].url == name: + if projname in self.public: + if self.public[projname].url == name: log.debug(f"Project {projname} already loaded from {name}") return projname - if self._public[projname].url is not None: + if self.public[projname].url is not None: raise ProjectError( - f"Cannot load project {projname} from {name}, already defined with {self._public[projname].url}" + f"Cannot load project {projname} from {name}, already defined with {self.public[projname].url}" ) log.debug( - f"Project {projname} loaded from {self._public[projname].path}, overriding" + f"Project {projname} loaded from {self.public[projname].path}, overriding" ) + # Ensure _public is loaded before direct assignment + if self._public is None: + self._load_public() + assert self._public is not None + if uri.scheme == "file": - self._public[name] = Project(self, name, path=uri.path) + self._public[name] = Project(self, name, path=Path(uri.path)) else: self._public[name] = Project(self, name, url=name) return name - def expand_names(self, pattern): + def expand_names(self, pattern: str) -> List[str]: try: return util.expand_pattern(pattern, self.names, "project") except Exception as ex: log.debug(f"Failed to expand '{pattern}'") raise ProjectError(f"Failed to expand '{pattern}': {ex}") - def get_packages(self, projects, target): + def get_packages( + self, projects: List[str], target: BuildTarget + ) -> Dict[str, "Package"]: packages = {} for proj in projects: @@ -143,7 +154,9 @@ class Projects: return packages - def eval_generic_packages(self, target, generic_packages): + def eval_generic_packages( + self, target: BuildTarget, generic_packages: List[str] + ) -> Dict[str, Package]: pkgs = {} needs_pypi = False needs_cpan = False @@ -181,15 +194,13 @@ class Project: :ivar projects: parent ``Projects`` instance """ - @property - def generic_packages(self): - - # lazy evaluation: load per-project generic package list when we actually need it - if self._generic_packages is None: - self._generic_packages = self._load_generic_packages() - return self._generic_packages - - def __init__(self, projects, name, path=None, url=None): + def __init__( + self, + projects: Projects, + name: str, + path: Optional[Path] = None, + url: Optional[str] = None, + ): self.projects = projects self.name = name self.path = path @@ -203,22 +214,35 @@ class Project: f"Only one of 'path' or 'url' can be present for project {name}" ) - self._generic_packages = None - self._target_packages = {} + self._generic_packages: Optional[List[str]] = None + self._target_packages: Dict[str, Dict[str, Package]] = {} - def _load_data(self): + @property + def generic_packages(self) -> List[str]: + if self._generic_packages is None: + self._generic_packages = self._load_generic_packages() + assert self._generic_packages is not None + return self._generic_packages + + def _load_data(self) -> str: if self.path is not None: with open(self.path, "r") as fh: return fh.read() else: + assert self.url is not None req = requests.get(self.url, stream=True) - return req.content + return req.content.decode("utf-8") @property - def location(self): - return self.path or self.url + def location(self) -> Union[Path, str]: + if self.path is not None: + return self.path + elif self.url is not None: + return self.url + else: + raise ProjectError(f"Project {self.name} has no path or url") - def _load_generic_packages(self): + def _load_generic_packages(self) -> List[str]: log.debug( f"Loading generic package list for project '{self.name}' from '{self.location}'" ) @@ -226,14 +250,19 @@ class Project: try: data = self._load_data() yaml_packages = yaml.safe_load(data) - return yaml_packages["packages"] + packages = yaml_packages["packages"] + if not isinstance(packages, list): + raise ProjectError( + f"Expected packages to be a list, got {type(packages)}" + ) + return packages except Exception as ex: log.debug(f"Can't load packages for '{self.name}' from '{self.location}'") raise ProjectError( f"Can't load packages for '{self.name}' from '{self.location}': {ex}" ) - def get_packages(self, target): + def get_packages(self, target: BuildTarget) -> Dict[str, Package]: osname = target.facts["os"]["name"] osversion = target.facts["os"]["version"] target_name = f"{osname.lower()}-{osversion.lower()}" @@ -243,7 +272,7 @@ class Project: try: util.validate_cross_platform(target.cross_arch, osname) except ValueError as ex: - raise ProjectError(ex) + raise ProjectError(str(ex)) target_name = f"{target_name}-{target.cross_arch}-cross" # lazy evaluation + caching of package names for a given distro diff --git a/pyproject.toml b/pyproject.toml index ae38a70..82bb19d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ files = [ "lcitool/gitlab.py", "lcitool/libvirt_wrapper.py", "lcitool/logger.py", + "lcitool/projects.py", "lcitool/targets.py", "lcitool/util.py", ] |