Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build targets for updating dependencies #1157

Merged
merged 8 commits into from
Dec 17, 2024
1 change: 1 addition & 0 deletions .changelog-bugfix.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
- Releases are now automated via Continuous Integration, including the update of the project's changelog.
- The presentation of algorithmic parameters in the documentation has been improved.
- Outdated GitHub Actions can now be printed via the build target `check_github_actions`. Alternatively, the build target `update_github_actions` may be used to update them automatically.
- The build target `update_dependencies` can now be used to update Python dependencies. The build targets `update_build_dependencies` and `update_runtime_dependencies` only update build-time and runtime dependencies, respectively.
- Continuous Integration is now used to automatically update outdated GitHub Actions on a regular schedule.
- Continuous Integration is now used to periodically update the Doxygen configuration file used for generating API documentations for C++ code.
8 changes: 7 additions & 1 deletion build_system/targets/dependencies/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@
from core.targets import PhonyTarget, TargetBuilder

from targets.dependencies.python.modules import DependencyType, PythonDependencyModule
from targets.dependencies.python.targets import CheckPythonDependencies, InstallRuntimeDependencies
from targets.dependencies.python.targets import CheckPythonDependencies, InstallRuntimeDependencies, \
UpdatePythonDependencies
from targets.paths import Project

VENV = 'venv'

TARGETS = TargetBuilder(BuildUnit.for_file(__file__)) \
.add_phony_target(VENV).set_runnables(InstallRuntimeDependencies()) \
.add_phony_target('check_runtime_dependencies').set_runnables(CheckPythonDependencies(DependencyType.RUNTIME)) \
.add_phony_target('check_build_dependencies').set_runnables(CheckPythonDependencies(DependencyType.BUILD_TIME)) \
.add_phony_target('check_dependencies').set_runnables(CheckPythonDependencies()) \
.add_phony_target('update_runtime_dependencies').set_runnables(UpdatePythonDependencies(DependencyType.RUNTIME)) \
.add_phony_target('update_build_dependencies').set_runnables(UpdatePythonDependencies(DependencyType.BUILD_TIME)) \
.add_phony_target('update_dependencies').set_runnables(UpdatePythonDependencies()) \
.build()

MODULES = [
Expand Down
84 changes: 67 additions & 17 deletions build_system/targets/dependencies/python/pip.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,33 @@

Provides classes for listing installed Python dependencies via pip.
"""
from dataclasses import dataclass
from dataclasses import dataclass, replace
from typing import Set

from util.pip import Package, Pip, Requirement
from util.pip import Package, Pip, RequirementsFile, RequirementVersion


@dataclass
class Dependency:
"""
Provides information about a dependency.
Provides information about an outdated dependency.

Attributes:
installed: The version of the dependency that is currently installed
latest: The latest version of the dependency
requirements_file: The path to the requirements file that defines the dependency
package: The package, the dependency corresponds to
outdated: The outdated version of the dependency
latest: The latest version of the dependency
"""
installed: Requirement
latest: Requirement
requirements_file: str
package: Package
outdated: RequirementVersion
latest: RequirementVersion

def __eq__(self, other: 'Dependency') -> bool:
return self.installed == other.installed
return self.requirements_file == other.requirements_file and self.package == other.package

def __hash__(self) -> int:
return hash(self.installed)
return hash((self.requirements_file, self.package))


class PipList(Pip):
Expand Down Expand Up @@ -55,6 +59,8 @@ def install_all_packages(self):
def list_outdated_dependencies(self) -> Set[Dependency]:
"""
Returns all outdated Python dependencies that are currently installed.

:return: A set that contains all outdated dependencies
"""
stdout = PipList.ListCommand(outdated=True).print_command(False).capture_output()
stdout_lines = stdout.strip().split('\n')
Expand All @@ -77,13 +83,57 @@ def list_outdated_dependencies(self) -> Set[Dependency]:
+ line)

package = Package(parts[0])
requirement = self.requirements.lookup_requirement(package, accept_missing=True)

if requirement and requirement.version:
installed_version = parts[1]
latest_version = parts[2]
outdated_dependencies.add(
Dependency(installed=Requirement(package, version=installed_version),
latest=Requirement(package, version=latest_version)))
requirements_by_file = self.requirements.lookup_requirement_by_file(package, accept_missing=True)

for requirements_file, requirement in requirements_by_file.items():
if requirement.version:
installed_version = parts[1]
latest_version = parts[2]
outdated_dependencies.add(
Dependency(requirements_file=requirements_file,
package=package,
outdated=RequirementVersion.parse(installed_version),
latest=RequirementVersion.parse(latest_version)))

return outdated_dependencies

def update_outdated_dependencies(self) -> Set[Dependency]:
"""
Updates all outdated Python dependencies that are currently installed.

:return: A set that contains all dependencies that have been updated
"""
updated_dependencies = set()
separator = '.'

for outdated_dependency in self.list_outdated_dependencies():
latest_version = outdated_dependency.latest
latest_version_parts = [int(part) for part in latest_version.min_version.split(separator)]
requirements_file = RequirementsFile(outdated_dependency.requirements_file)
package = outdated_dependency.package
outdated_requirement = requirements_file.lookup_requirement(package)
outdated_version = outdated_requirement.version
updated_version = latest_version

if outdated_version.is_range():
min_version_parts = [int(part) for part in outdated_version.min_version.split(separator)]
max_version_parts = [int(part) for part in outdated_version.max_version.split(separator)]
num_version_numbers = min(len(min_version_parts), len(max_version_parts), len(latest_version_parts))

for i in range(num_version_numbers):
min_version_parts[i] = latest_version_parts[i]
max_version_parts[i] = latest_version_parts[i]

max_version_parts[num_version_numbers - 1] += 1
updated_version = RequirementVersion(
min_version=separator.join([str(part) for part in min_version_parts[:num_version_numbers]]),
max_version=separator.join([str(part) for part in max_version_parts[:num_version_numbers]]))

requirements_file.update(replace(outdated_requirement, version=updated_version))
updated_dependencies.add(
Dependency(requirements_file=outdated_dependency.requirements_file,
package=package,
outdated=outdated_version,
latest=updated_version))

return updated_dependencies
58 changes: 51 additions & 7 deletions build_system/targets/dependencies/python/targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Implements targets for installing runtime requirements that are required by the project's source code.
"""
from functools import reduce
from typing import List
from typing import List, Optional

from core.build_unit import BuildUnit
from core.modules import Module
Expand Down Expand Up @@ -34,25 +34,69 @@ class CheckPythonDependencies(PhonyTarget.Runnable):
Installs all Python dependencies used by the project and checks for outdated ones.
"""

def __init__(self):
super().__init__(PythonDependencyModule.Filter())
def __init__(self, dependency_type: Optional[DependencyType] = None):
"""
:param dependency_type: The type of the Python dependencies to be checked or None, if all dependencies should be
updated
"""
super().__init__(
PythonDependencyModule.Filter(dependency_type) if dependency_type else PythonDependencyModule.Filter())
self.dependency_type = dependency_type

def run_all(self, build_unit: BuildUnit, modules: List[Module]):
requirements_files = reduce(lambda aggr, module: aggr + module.find_requirements_files(), modules, [])
pip = PipList(*requirements_files)
Log.info('Installing all dependencies...')
Log.info('Installing %s dependencies...',
('all build-time' if self.dependency_type == DependencyType.BUILD_TIME else 'all runtime')
if self.dependency_type else 'all')
pip.install_all_packages()
Log.info('Checking for outdated dependencies...')
outdated_dependencies = pip.list_outdated_dependencies()

if outdated_dependencies:
table = Table(build_unit, 'Dependency', 'Installed version', 'Latest version')
table = Table(build_unit, 'Dependency', 'Requirements file', 'Installed version', 'Latest version')

for outdated_dependency in outdated_dependencies:
table.add_row(str(outdated_dependency.installed.package), outdated_dependency.installed.version,
outdated_dependency.latest.version)
table.add_row(str(outdated_dependency.package), outdated_dependency.requirements_file,
outdated_dependency.outdated.min_version, outdated_dependency.latest.min_version)

table.sort_rows(0, 1)
Log.info('The following dependencies are outdated:\n\n%s', str(table))
else:
Log.info('All dependencies are up-to-date!')


class UpdatePythonDependencies(PhonyTarget.Runnable):
"""
Installs all Python dependencies of a specific type and updates outdated ones.
"""

def __init__(self, dependency_type: Optional[DependencyType] = None):
"""
:param dependency_type: The type of the Python dependencies to be updated or None, if all dependencies should be
updated
"""
super().__init__(
PythonDependencyModule.Filter(dependency_type) if dependency_type else PythonDependencyModule.Filter())
self.dependency_type = dependency_type

def run_all(self, build_unit: BuildUnit, modules: List[Module]):
requirements_files = reduce(lambda aggr, module: aggr + module.find_requirements_files(), modules, [])
pip = PipList(*requirements_files)
Log.info('Installing %s dependencies...',
('all build-time' if self.dependency_type == DependencyType.BUILD_TIME else 'all runtime')
if self.dependency_type else 'all')
pip.install_all_packages()
Log.info('Updating outdated dependencies...')
updated_dependencies = pip.update_outdated_dependencies()

if updated_dependencies:
table = Table(build_unit, 'Dependency', 'Requirements file', 'Previous version', 'Updated version')
for updated_dependency in updated_dependencies:
table.add_row(str(updated_dependency.package), updated_dependency.requirements_file,
str(updated_dependency.outdated), str(updated_dependency.latest))

table.sort_rows(0, 1)
Log.info('The following dependencies have been updated:\n\n%s', str(table))
else:
Log.info('No dependencies must be updated!')
Loading
Loading