Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ default_language_version:

repos:
- repo: https://github.com/psf/black
rev: 24.10.0
rev: 25.1.0
hooks:
- id: black
language_version: python3
Expand All @@ -12,14 +12,14 @@ repos:
rev: 1.19.1
hooks:
- id: blacken-docs
additional_dependencies: [black==23.9.1]
additional_dependencies: [black==25.1.0]
- repo: https://github.com/pycqa/isort
# isort config is in setup.cfg
rev: 5.13.2
rev: 6.0.1
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
# flake8 config is in setup.cfg
rev: 7.1.1
rev: 7.3.0
hooks:
- id: flake8
108 changes: 70 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,87 +1,119 @@
# Python dependency version checker
# Dependency Checker

![Icon](./docs/icon.png?raw=true "Icon")

A tool to report outdated dependencies in Python projects using Poetry.

## Table of Contents

- [Why use this tool?](#why-use-this-tool)
- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
- [Options](#options)
- [How it works](#how-it-works)
- [Limitations](#limitations)

## Why use this tool?

If you are using Poetry for python dependency management it can help you to decide if you need to update a dependency or not.
If you're using Poetry for Python dependency management, this tool helps you decide whether you need to update dependencies.

e.g. your `pyproject.toml` file may have a version range specified, but it may not be clear if the version in the lock file is the latest allowed by your range in the pyproject.toml file
For example, your `pyproject.toml` file may have a version range specified, but it may not be clear if the version in the lock file is the latest allowed by your range in the `pyproject.toml` file.

You could run poetry show [dependency] to get the installed version, then pop over to PyPi to check the latest version but if you have a lot of dependencies, this can be time-consuming, so let this tool do it for you.
You could run `poetry show [dependency]` to get the installed version, then check PyPI for the latest version. However, if you have many dependencies, this can be time-consuminglet this tool do it for you.

## Requirements

- Python 3.11+
- UV https://docs.astral.sh/uv/
- [UV](https://docs.astral.sh/uv/) - Python package manager

## Installation

Clone this repository and run the following commands in the root of the project:
1. Clone this repository:

## Activate the virtual environment
```bash
git clone https://github.com/nm-examples/dependency-checker.git
cd dependency-checker
```

This isn't strictly necessary but it is recommended to allow command completion for folder paths when checking a local repository.
2. Create and activate a virtual environment (optional but recommended):

```
uv venv
source .venv/bin/activate
```
```bash
uv venv
source .venv/bin/activate
```

This step isn't strictly necessary but is recommended to enable command completion for folder paths when checking local repositories.

## Usage

### Basic Commands

Without activating the virtual environment:

```
```bash
uv run check [-r] [local or remote]
```

With the virtual environment activated:

```
```bash
check [-r] [local or remote]
```

If you run `uv check` without any arguments, it will display the help docs.
If you run the command without any arguments, it will display the help documentation.

Steps:
### Workflow

- Enter the url for your remote repository or the path to your local repository
- Choose the branch to checkout and run the report on
- If multiple Dockerfiles are found, choose the one to inspect
1. Enter the URL for your remote repository or the path to your local repository
2. Choose the branch to checkout and run the report on
3. If multiple Dockerfiles are found, choose the one to inspect

## Options

`-r` - Output a printable report to a file (report.html) View with `open report.html`

`local` - Check a local repository (a folder relative to the directory this script is run from)
| Option | Description |
|--------|-------------|
| `-r` | Output a printable report to a file (`report.html`). View with `open report.html` |
| `local` | Check a local repository (a folder relative to the directory this script is run from) |
| `remote` | Check a remote repository |

`remote` - Check a remote repository
### Getting Help

Command help is available:
Command help is available for all commands:

```
```bash
uv run check --help
uv run check remote --help
uv run check local --help
```

## Limitations
## How it works

- Only works if the Dockerfile uses poetry to install dependencies
The tool performs the following steps:

## How it works
1. **Repository handling**:
- Clones the repository and checks out the specified branch (for remote repositories)
- Analyzes the local folder (for local repositories)

2. **Docker analysis**:
- Finds and inspects the Dockerfile to identify the Docker image version and Poetry version used

It will do the following:
3. **Dependency extraction**:
- Builds a new image based on the Dockerfile
- Exports the dependency list using `poetry export` to `requirements-frozen.txt`

- clone the repository and checkout the specified branch for a remote repository
- analyse the local folder if running against a local repository
- find & inspect the Dockerfile to find the docker image version and poetry version used
- build a new image based on the Dockerfile image and export the dependency list using poetry export -> requirements-frozen.txt
- compare each dependency version in requirements-frozen.txt with the latest version on PyPi if it is listed in the pyproject.toml file
- output the results in the console and indicate if there are any outdated dependencies and/or manual checks required
- optionally output a report to a file
4. **Version comparison**:
- Compares each dependency version in `requirements-frozen.txt` with the latest version on PyPI
- Only checks dependencies listed in the `pyproject.toml` file

e.g. Console output
5. **Reporting**:
- Outputs results in the console indicating outdated dependencies and manual checks required
- Optionally generates an HTML report file

### Example Console Output

![Console output](./docs/screen.jpg "Console output")

## Limitations

![Console ouput](./docs/console.jpg?raw=true "Console output")
- Only works if the Dockerfile uses Poetry to install dependencies
Binary file removed docs/console.jpg
Binary file not shown.
Binary file removed docs/console.png
Binary file not shown.
Binary file added docs/screen.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies = [
[dependency-groups]
dev = [
"black>=24.10.0",
"blacken-docs>=1.19.1",
"flake8>=7.1.1",
"isort>=5.13.2",
"pre-commit>=4.0.1",
Expand Down
4 changes: 2 additions & 2 deletions src/check_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ def check_local(path: str, report: bool) -> None:

messages = []

# production dependencies
# main dependencies
dependencies = sorted(toml.dependencies.keys())
table = dependency_table_header(title="Production Dependencies")
table = dependency_table_header(title="Main Dependencies")

production_packages = get_packages(dependencies, messages)

Expand Down
4 changes: 2 additions & 2 deletions src/check_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ def check_remote(repo_url, report):

messages = []

# production dependencies
# Main dependencies
dependencies = sorted(toml.dependencies.keys())
table = dependency_table_header(title="Production Dependencies")
table = dependency_table_header(title="Main Dependencies")

production_packages = get_packages(dependencies, messages)

Expand Down
60 changes: 53 additions & 7 deletions src/helpers.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,77 @@
from packaging.version import InvalidVersion, parse
from rich import box
from rich.table import Table
from src.client import PyPiClient
from src.managers.package import Package


def calculate_update_type(current_version, latest_version):
"""
Calculate if the latest version is a major, minor, or patch update compared to the current version.
Returns one of: 'major', 'minor', 'patch', or None.
"""
try:
current = parse(current_version)
latest = parse(latest_version)
except InvalidVersion:
return None
if current == latest:
return None
if hasattr(current, "major") and hasattr(latest, "major"):
if latest.major > current.major:
return "major"
elif latest.minor > current.minor:
return "minor"
elif latest.micro > current.micro:
return "patch"
return None


def package_table_row(frozen, package):
name = package.name
latest_version = package.latest_version
frozen_version = frozen.dependencies.get(name.lower())

# Calculate update_type based on frozen version vs latest version
update_type = None
if frozen_version and frozen_version != latest_version and "git+https://" not in frozen_version:
update_type = calculate_update_type(frozen_version, latest_version)

if frozen_version and frozen_version != latest_version:
if "git+https://" in frozen_version:
status = "Check"
style = "red1"
style = "white"
frozen_version = "Forked package"
else:
status = "Outdated"
style = "yellow1"
# Set color based on update type
if update_type == "major":
style = "red1"
elif update_type == "minor":
style = "yellow1"
elif update_type == "patch":
style = "orange1"
else:
style = "yellow1" # fallback for when update_type is None
elif not frozen_version:
status = "Check"
style = "cyan1"
frozen_version = "Unable to determine version"
style = "white"
frozen_version = "Unknown version"
else:
status = "OK"
style = "green3"

if "git+https://" in frozen_version:
frozen_version = frozen_version.replace("git+https://", "")
frozen_version = f"{frozen_version.split('@')[0]} TAG {frozen_version.split('@')[1]}"
# Always reflect update_type if present
if update_type:
status = f"{status} ({update_type})"

# If it's a major update, add the repository URL if available
if update_type == "major":
repo_url = package.repository_url
if repo_url:
status = f"{status}\n{repo_url}"

# No need to process git URLs anymore since we set frozen_version to "Forked package"
return name, latest_version, frozen_version, status, style


Expand Down
14 changes: 14 additions & 0 deletions src/managers/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ def latest_version(self):
version_str = self.parse_versions_for_latest().__str__()
return version_str

@property
def repository_url(self):
"""
Returns the repository URL from project_urls in order of preference:
Changelog, Changes, Release log, Repository, then Source. Returns the first one found or None.
"""
project_urls = self.json["info"].get("project_urls", {})
if isinstance(project_urls, dict):
# Check in order of preference
for key in ["Changelog", "Changes", "Release log", "Repository", "Source"]:
if key in project_urls:
return project_urls[key]
return None

def parse_versions_for_latest(self):
"""
Parse the versions and return the latest version
Expand Down
Loading