Skip to content

Commit

Permalink
Update to v0.9.4
Browse files Browse the repository at this point in the history
  • Loading branch information
HubTou authored Mar 31, 2023
1 parent 66a46f2 commit 9361666
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 66 deletions.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,3 @@ It is available under the [3-clause BSD license](https://opensource.org/licenses
## AUTHORS
[Hubert Tournier](https://github.com/HubTou)

## CAVEATS
The conditions on package dependencies aren't taken into account (yet).
"pipinfo -N" will nonetheless give appoximately the same results than "pip list --not-required".

2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Ideas for improvement and evolution

## Limitations to be removed
* Processing dependencies conditions, including testing variables such as:
os_name, platform_python_implementation, platform_system, sys_platform, python_version, python_full_version, implementation_name
* Handling variations ('-' or '_' instead of the other one) in package dependencies

## New features
Expand Down
6 changes: 1 addition & 5 deletions man/pipinfo.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.Dd March 21, 2023
.Dd March 31, 2023
.Dt PIPINFO 1
.Os
.Sh NAME
Expand Down Expand Up @@ -193,7 +193,3 @@ both for my personal convenience and also to investigate some pip issues with th
It is available under the 3-clause BSD license.
.Sh AUTHORS
.An Hubert Tournier
.Sh CAVEATS
The conditions on package dependencies aren't taken into account (yet).
.Pp
"pipinfo -N" will nonetheless give appoximately the same results than "pip list --not-required".
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = pnu-pipinfo
description = Alternative tool for listing Python packages
long_description = file: README.md
long_description_content_type = text/markdown
version = 0.9.3
version = 0.9.4
license = BSD 3-Clause License
license_files = License
author = Hubert Tournier
Expand Down
170 changes: 117 additions & 53 deletions src/pipinfo/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import logging
import os
import platform
import pprint
import re
import sys
Expand Down Expand Up @@ -43,6 +44,16 @@ def get_site_package_dirs():
return site_packages_dirs


####################################################################################################
def _clean_condition(condition):
""" Strip spaces and remove parenthesis from a requirement condition """
condition = condition.strip()
if condition[0] == '(':
condition = condition[1:-1]

return condition


####################################################################################################
def process_requires_file(filename, requires, extras):
""" Loads requires and extras from a .egg-info/requires.txt file """
Expand Down Expand Up @@ -77,20 +88,27 @@ def process_requires_file(filename, requires, extras):
conditions = re.sub(r"^.*] *;* *", "", line)
else:
conditions = re.sub(r"^" + dependency + " *;* *", "", line)
conditions = re.sub(r" +and +", ";", conditions)
conditions = re.sub(r" *, *", ";",conditions)

if extra:
if dependency not in extras[extra]:
extras[extra][dependency] = []
if conditions:
extras[extra][dependency].append(conditions)
if extra_conditions:
extras[extra][dependency].append(extra_conditions)
for condition in conditions.split(";"):
if condition:
extras[extra][dependency].append(_clean_condition(condition))
for condition in extra_conditions.split(";"):
if condition:
extras[extra][dependency].append(_clean_condition(condition))
else:
if dependency not in requires:
requires[dependency] = []
if conditions:
requires[dependency].append(conditions)
if extra_conditions:
requires[dependency].append(extra_conditions)
for condition in conditions.split(";"):
if condition:
requires[dependency].append(_clean_condition(condition))
for condition in extra_conditions.split(";"):
if condition:
requires[dependency].append(_clean_condition(condition))

logging.debug("requires:\n%s", pprint.pformat(requires))
logging.debug("extras:\n%s", pprint.pformat(extras))
Expand Down Expand Up @@ -153,33 +171,39 @@ def get_info_from_site_packages_dir(directory, directory_type):
conditions = re.sub(r"^.*] *;* *", "", line)
else:
conditions = re.sub(r"^" + dependency + " *;* *", "", line)
conditions = re.sub(r" +and +", ";", conditions)
conditions = re.sub(r" *, *", ";",conditions)

if "extra == " in line:
for part in conditions.split("extra == ")[1:]:
extra = re.sub(r"['\"].*", "", part[1:])

# Remove the extra == "NAME" from the conditions
while "extra == " in conditions:
conditions = re.sub(r" *;* *(and|or)* *extra == ('[^']*'|\"[^\"]*\") *(and|or)* *", ";", conditions)
conditions = re.sub(r" *;* *(or)* *extra == ('[^']*'|\"[^\"]*\") *(or)* *", ";", conditions)
conditions = re.sub(r";*$", "", conditions)

if extra not in extras:
extras[extra] = {}
if dependency in extras[extra]:
if conditions:
extras[extra][dependency].append(conditions)
elif conditions:
extras[extra][dependency] = [conditions]
for condition in conditions.split(";"):
if condition:
extras[extra][dependency].append(_clean_condition(condition))
else:
extras[extra][dependency] = []
for condition in conditions.split(";"):
if condition:
extras[extra][dependency].append(_clean_condition(condition))
else:
if dependency in requires:
if conditions:
requires[dependency].append(conditions)
elif conditions:
requires[dependency] = [conditions]
for condition in conditions.split(";"):
if condition:
requires[dependency].append(_clean_condition(condition))
else:
requires[dependency] = []
for condition in conditions.split(";"):
if condition:
requires[dependency].append(_clean_condition(condition))
elif line.startswith("Home-page: "):
pass
elif line.startswith("Project-URL: "):
Expand Down Expand Up @@ -502,6 +526,68 @@ def is_package_vulnerable(package, vulnerabilities):
and package['version'] in vulnerabilities[package['name']]


####################################################################################################
def _verify_conditions(name, dependency, conditions):
""" """
if not conditions:
return True

for condition in conditions:
if condition[0].isalpha():
# Splitting the condition in a [string, operator, value] triplet
part = condition.split()
if len(part) != 3:
condition = re.sub(r" +", " ", condition)
condition = re.sub(r"([A-Za-z_]+) *([<=>]=*) *(.*)", r"\1 \2 \3", condition)
part = condition.split()
if len(part) != 3:
logging.warning("Condition '%s' for dependency '%s' of package '%s' doesn't have 3 parts. Please report it!", condition, dependency, name)
return False

value = ''
if part[0] == 'implementation_name':
value = sys.implementation.name
elif part[0] == 'os_name':
value = os.name
elif part[0] == 'platform_python_implementation':
value = platform.python_implementation()
elif part[0] == 'platform_system':
value = platform.system()
elif part[0] == 'python_full_version':
value = platform.python_version()
elif part[0] == 'python_version':
value = platform.python_version_tuple()[0] + '.' + platform.python_version_tuple()[1]
elif part[0] == 'sys_platform':
value = sys.platform
else:
logging.warning("Unknown condition string '%s' for dependency '%s' of package '%s'. Please report it!", condition, dependency, name)
return False

if part[2][0] in ("'", '"'):
part[2] = part[2][1:-1]

if part[1] == '==':
if value != part[2]:
return False
elif part[1] == '<':
if packaging.version.parse(value) >= packaging.version.parse(part[2]):
return False
elif part[1] == '<=':
if packaging.version.parse(value) > packaging.version.parse(part[2]):
return False
elif part[1] == '>':
if packaging.version.parse(value) <= packaging.version.parse(part[2]):
return False
elif part[1] == '>=':
if packaging.version.parse(value) < packaging.version.parse(part[2]):
return False
else:
logging.warning("Unknown condition operator '%s' for dependency '%s' of package '%s'. Please report it!", condition, dependency, name)
return False

return True


####################################################################################################
def get_packages_required_by(packages):
""" Returns a dictionary of packages which are required by others """
Expand All @@ -511,7 +597,7 @@ def get_packages_required_by(packages):
# All comparisons are done case insensitive as packages are usually not precise...
for package in packages:
name = package['name'].lower()
for dependency in package['requires']:
for dependency, conditions in package['requires'].items():
dependency = dependency.lower()
# A dependency can reference packages options ("extras") within brackets
if '[' in dependency:
Expand All @@ -526,35 +612,12 @@ def get_packages_required_by(packages):
else:
extras[dependency] = [part]

""" TODO The dependency conditions are not considered as of now
Variables seen:
os_name
from: os.name
values: 'nt', 'posix'
platform_python_implementation
from: platform.python_implementation()
values: 'CPython', 'PyPy'
platform_system
from: platform.system()
values: 'Windows', 'FreeBSD'
sys_platform
from: sys.platform
values: 'linux', 'win32', 'freebsd13'
python_version
from: platform.python_version_tuple()[0] + '.' + platform.python_version_tuple()[1]
values: '2.7', '3', '3.9'
python_full_version
from: platform.python_version()
values: '3.9.16'
implementation_name
from: sys.implementation.name
values: 'cpython'
"""
if dependency in required_by:
if name not in required_by[dependency]:
required_by[dependency].append(name)
else:
required_by[dependency] = [name]
if _verify_conditions(name, dependency, conditions):
if dependency in required_by:
if name not in required_by[dependency]:
required_by[dependency].append(name)
else:
required_by[dependency] = [name]

# If we have encountered extras, let's try to add their new dependencies
while extras:
Expand All @@ -566,7 +629,7 @@ def get_packages_required_by(packages):
if key == name:
for extra in value:
if extra in package['extras']:
for dependency in package['extras'][extra]:
for dependency, conditions in package['extras'][extra].items():
dependency = dependency.lower()
# A dependency can reference packages options ("extras") within brackets
if '[' in dependency:
Expand All @@ -581,11 +644,12 @@ def get_packages_required_by(packages):
else:
extras[dependency] = [part]

if dependency in required_by:
if name not in required_by[dependency]:
required_by[dependency].append(name)
else:
required_by[dependency] = [name]
if _verify_conditions(name, dependency, conditions):
if dependency in required_by:
if name not in required_by[dependency]:
required_by[dependency].append(name)
else:
required_by[dependency] = [name]

del extras[key]

Expand Down
2 changes: 1 addition & 1 deletion src/pipinfo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
get_packages_required_by, is_package_required, list_packages

# Version string used by the what(1) and ident(1) commands:
ID = "@(#) $Id: pipinfo - Alternative tool for listing Python packages v0.9.3 (March 31, 2023) by Hubert Tournier $"
ID = "@(#) $Id: pipinfo - Alternative tool for listing Python packages v0.9.4 (March 31, 2023) by Hubert Tournier $"

# Default parameters. Can be overcome by environment variables, then command line options
parameters = {
Expand Down

0 comments on commit 9361666

Please sign in to comment.