Skip to content
This repository was archived by the owner on Jan 20, 2024. It is now read-only.

Commit 0339c49

Browse files
committed
## 2023
- checkrequirements is now a thin wrapper around poetry show and sets an exit code only. **Note:** this only supports poetry based environments. `requirements.txt` and other build systems are no longer supported!
1 parent 9d448b2 commit 0339c49

File tree

11 files changed

+35
-676
lines changed

11 files changed

+35
-676
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
All major and minor version changes will be documented in this file. Details of
44
patch-level version changes can be found in [commit messages](../../commits/master).
55

6+
## 2023
7+
8+
- checkrequirements is now a thin wrapper around poetry show and sets an exit code only. **Note:** this only supports poetry based environments. `requirements.txt` and other build systems are no longer supported!
9+
610
## 2022.0.1 - 2022/04/06
711

812
- Remove metprint

README.md

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,13 @@
1313

1414
<img src="readme-assets/icons/name.png" alt="Project Icon" width="750">
1515

16-
Check that your requirements.txt is up to date with the most recent package
17-
versions
16+
Uses poetry to check outdated dependencies and set a status code to 1 if outdated packages are found. This is ideal for ci/cd. If running manually, its better to use poetry directly.
1817

19-
## Example Use
20-
21-
See below for the output if you run `checkrequirements` in this directory
22-
23-
```txt
24-
>> checkrequirements
25-
+ OK: requests
26-
+ OK: requirements-parser
27-
```
28-
29-
### Help
30-
31-
```txt
32-
usage: __main__.py [-h] [--requirements-file REQUIREMENTS_FILE]
33-
34-
Check that your requirements.txt is up to date with the most recent package versions
35-
36-
optional arguments:
37-
-h, --help show this help message and exit
38-
--requirements-file REQUIREMENTS_FILE, -r REQUIREMENTS_FILE
39-
requirements file
40-
```
41-
42-
You can also import this into your own project and use any of the functions
43-
in the DOCS
18+
**Note:** this only supports poetry based environments. `requirements.txt` and other build systems are not currently supported!
4419

4520
<!-- omit in toc -->
4621
## Table of Contents
4722

48-
- [Example Use](#example-use)
49-
- [Help](#help)
50-
- [Documentation](#documentation)
5123
- [Install With PIP](#install-with-pip)
5224
- [Language information](#language-information)
5325
- [Built for](#built-for)
@@ -79,23 +51,6 @@ in the DOCS
7951
- [Support](#support)
8052
- [Rationale](#rationale)
8153

82-
## Documentation
83-
84-
A high-level overview of how the documentation is organized organized will help you know
85-
where to look for certain things:
86-
87-
<!--
88-
- [Tutorials](/documentation/tutorials) take you by the hand through a series of steps to get
89-
started using the software. Start here if you’re new.
90-
-->
91-
- The [Technical Reference](/documentation/reference) documents APIs and other aspects of the
92-
machinery. This documentation describes how to use the classes and functions at a lower level
93-
and assume that you have a good high-level understanding of the software.
94-
<!--
95-
- The [Help](/documentation/help) guide provides a starting point and outlines common issues that you
96-
may have.
97-
-->
98-
9954
## Install With PIP
10055

10156
```python

checkrequirements/__init__.py

Lines changed: 15 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,236 +1,20 @@
1-
"""Check that your requirements.txt is up to date with the most recent packageversions.
2-
"""
3-
from __future__ import annotations
1+
import subprocess
2+
import sys
43

5-
import argparse
6-
import typing
7-
from pathlib import Path
8-
from sys import exit as sysexit
9-
from sys import stdout
104

11-
import requests
12-
import requirements
13-
from requirements.requirement import Requirement
14-
15-
stdout.reconfigure(encoding="utf-8")
16-
17-
18-
class UpdateCompatible(typing.TypedDict):
19-
"""UpdateCompatible type."""
20-
21-
ver: str
22-
compatible: bool
23-
24-
25-
class Dependency(typing.TypedDict):
26-
"""Dependency type."""
27-
28-
name: str
29-
specs: tuple[str]
30-
ver: str
31-
compatible: bool
32-
33-
34-
def semver(version: str) -> list[str]:
35-
"""Convert a semver/ python-ver string to a list in the form major, minor patch
36-
37-
Args:
38-
version (str): The version to convert
39-
40-
Returns:
41-
list[str]: A list in the form major, minor, patch
42-
"""
43-
return version.split(".")
44-
45-
46-
def semPad(ver: list[str], length: int) -> list[str]:
47-
"""Pad a semver list to the required size. e.g. ["1", "0"] to ["1", "0", "0"].
48-
49-
Args:
50-
ver (list[str]): the semver representation
51-
length (int): the new length
52-
53-
Returns:
54-
list[str]: the new semver
55-
"""
56-
char = "0"
57-
if ver[-1] == "*":
58-
char = "*"
59-
return ver + [char] * (length - len(ver))
60-
61-
62-
def partCmp(verA: str, verB: str) -> int:
63-
"""Compare parts of a semver.
64-
65-
Args:
66-
verA (str): lhs part to compare
67-
verB (str): rhs part to compare
68-
69-
Returns:
70-
int: 0 if equal, 1 if verA > verB and -1 if verA < verB
71-
"""
72-
if verA == verB or verA == "*" or verB == "*":
73-
return 0
74-
if int(verA) > int(verB):
75-
return 1
76-
return -1
77-
78-
79-
def _doSemCmp(semA: list[str], semB: list[str], sign: str) -> bool:
80-
"""Compare two semvers of equal length. e.g. 1.1.1 and 2.2.2.
81-
82-
Args:
83-
semA (list[str]): lhs to compare
84-
semB (list[str]): rhs to compare
85-
sign (str): string sign. one of ==, ~=, <=, >=, <, >
86-
87-
Raises:
88-
ValueError: if the sign is not one of the following. or the semvers
89-
have differing lengths
90-
91-
Returns:
92-
bool: true if the comparison is met. e.g. 1.1.1, 2.2.2, <= -> True
93-
"""
94-
if len(semA) != len(semB):
95-
raise ValueError
96-
# Equal. e.g. 1.1.1 == 1.1.1
97-
if sign == "==":
98-
for index, _elem in enumerate(semA):
99-
if partCmp(semA[index], semB[index]) != 0:
100-
return False
101-
return True
102-
# Compatible. e.g. 1.1.2 ~= 1.1.1
103-
if sign == "~=":
104-
for index, _elem in enumerate(semA[:-1]):
105-
if partCmp(semA[index], semB[index]) != 0:
106-
return False
107-
if partCmp(semA[-1], semB[-1]) < 0:
108-
return False
109-
return True
110-
# Greater than or equal. e.g. 1.1.2 >= 1.1.1
111-
if sign == ">=":
112-
for index, _elem in enumerate(semA):
113-
cmp = partCmp(semA[index], semB[index])
114-
if cmp > 0:
115-
return True
116-
if cmp < 0:
117-
return False
118-
return True
119-
# Less than or equal. e.g. 1.1.1 <= 1.1.2
120-
if sign == "<=":
121-
for index, _elem in enumerate(semA):
122-
cmp = partCmp(semA[index], semB[index])
123-
if cmp < 0:
124-
return True
125-
if cmp > 0:
126-
return False
127-
return True
128-
# Greater than. e.g. 1.1.2 > 1.1.1
129-
if sign == ">":
130-
for index, _elem in enumerate(semA):
131-
cmp = partCmp(semA[index], semB[index])
132-
if cmp > 0:
133-
return True
134-
if cmp < 0:
135-
return False
136-
return False
137-
# Less than. e.g. 1.1.1 < 1.1.2
138-
if sign == "<":
139-
for index, _elem in enumerate(semA):
140-
cmp = partCmp(semA[index], semB[index])
141-
if cmp < 0:
142-
return True
143-
if cmp > 0:
144-
return False
145-
return False
146-
raise ValueError
147-
148-
149-
def semCmp(versionA: str, versionB: str, sign: str) -> bool:
150-
"""Compare two semvers of any length. e.g. 1.1 and 2.2.2.
151-
152-
Args:
153-
versionA (list[str]): lhs to compare
154-
versionB (list[str]): rhs to compare
155-
sign (str): string sign. one of ==, ~=, <=, >=, <, >
156-
157-
Raises:
158-
ValueError: if the sign is not one of the following.
159-
160-
Returns:
161-
bool: true if the comparison is met. e.g. 1.1.1, 2.2.2, <= -> True
162-
"""
163-
semA = semver(versionA)
164-
semB = semver(versionB)
165-
semLen = max(len(semA), len(semB))
166-
return _doSemCmp(semPad(semA, semLen), semPad(semB, semLen), sign)
167-
168-
169-
def updateCompatible(req: Requirement) -> UpdateCompatible:
170-
"""Check if the most recent version of a python requirement is compatible with
171-
the current version.
172-
173-
Args:
174-
req (Requirement): the requirement object as parsed by requirements_parser
175-
176-
Returns:
177-
UpdateCompatible: return a dict of the most recent version (ver) and
178-
is our requirement from requirements.txt or similar compatible
179-
with the new version per the version specifier (compatible)
180-
"""
181-
url = f"https://pypi.org/pypi/{req.name}/json"
182-
request = requests.get(url)
183-
updateVer = request.json()["info"]["version"]
184-
for spec in req.specs:
185-
if not semCmp(updateVer, spec[1], spec[0]):
186-
return {"ver": updateVer, "compatible": False}
187-
return {"ver": updateVer, "compatible": True}
188-
189-
190-
def checkRequirements(requirementsFile: str) -> list[Dependency]:
191-
"""Check that your requirements.txt is up to date with the most recent package
192-
versions. Put in a function so dependants can use this function rather than
193-
reimplement it themselves.
194-
195-
Args:
196-
requirementsFile (str): file path to the requirements file
197-
198-
Returns:
199-
Dependency: dictionary containing info on each requirement such as the name,
200-
specs (from requirements_parser), ver (most recent version), compatible
201-
(is our version compatible with ver)
202-
"""
203-
reqsDict = []
204-
for req in requirements.parse(Path(requirementsFile).read_text(encoding="utf-8")): # type: ignore
205-
reqsDict.append(
206-
{"name": req.name, "specs": req.specs, **updateCompatible(req)}
207-
) # type: ignore
208-
return reqsDict
5+
def checkForOutdatedPackages():
6+
cmd = ["poetry", "show", "--outdated"]
7+
result = subprocess.run(cmd, capture_output=True, text=True)
8+
return result.stdout.strip().split("\n")[1:]
2099

21010

21111
def cli():
212-
"""CLI entry point."""
213-
parser = argparse.ArgumentParser(description=__doc__)
214-
# yapf: disable
215-
parser.add_argument("--requirements-file", "-r",
216-
help="requirements file")
217-
parser.add_argument("--zero", "-0",
218-
help="Return non zero exit code if an incompatible license is found", action="store_true")
219-
# yapf: enable
220-
args = parser.parse_args()
221-
reqsDict = checkRequirements(
222-
args.requirements_file if args.requirements_file else "requirements.txt"
223-
)
224-
if len(reqsDict) == 0:
225-
print("/ WARN: No requirements")
226-
incompat = False
227-
for req in reqsDict:
228-
name = req["name"]
229-
if req["compatible"]:
230-
print(f"+ OK: {name}")
231-
else:
232-
print(f"+ ERROR: {name}")
233-
incompat = True
234-
if incompat and args.zero:
235-
sysexit(1)
236-
sysexit(0)
12+
outdatedPackages = checkForOutdatedPackages()
13+
if outdatedPackages:
14+
print("Outdated packages (powered by poetry):")
15+
for package in outdatedPackages:
16+
print(package)
17+
else:
18+
print("No outdated packages.")
19+
20+
sys.exit(1 if outdatedPackages else 0)

0 commit comments

Comments
 (0)