aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc-André Lureau <marcandre.lureau@redhat.com>2025-09-02 12:08:14 +0400
committerMarc-André Lureau <marcandre.lureau@redhat.com>2025-09-03 16:18:12 +0400
commit84e464f19226ff2d41f4910fa66ea2ca94a3b1ed (patch)
tree4e57ac23922daf0dfd7b95a9471be11cc211c551
parent924025f6222c59c49fa9bd9fc26a00599c887bc6 (diff)
downloadlibvirt-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.py109
-rw-r--r--pyproject.toml1
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",
]