mach-nix/mach_nix/requirements.py

213 lines
6.3 KiB
Python
Raw Normal View History

2020-11-18 18:09:56 +00:00
import re
from typing import Iterable, Tuple, List
2020-04-22 09:28:58 +00:00
import distlib.markers
import pkg_resources
from conda.models.version import ver_eval
2020-04-22 09:28:58 +00:00
from distlib.markers import DEFAULT_CONTEXT
from mach_nix.cache import cached
from mach_nix.versions import PyVer, Version, parse_ver
2020-04-22 09:28:58 +00:00
version 2.2.0 (#60) # 2.2.0 (09 Aug 2020) Improved success rate, MacOS support, bugfixes, optimizations ### Features - Improved selection of wheel releases. MacOS is now supported and architectures besides x86_64 should be handled correctly. - Whenever mach-nix resolves dependencies, a visualization of the resulting dependency tree is printed on the terminal. - The dependency DB is now accessed through a caching layer which reduces the resolver's CPU time significantly for larger environments. - The python platform context is now generated from the nix build environment variable `system`. This should decrease the chance of impurities during dependency resolution. ### Fixes - The requires_python attribute of wheels was not respected. This lead to failing builds especially for older python versions. Now `requires_python` is part of the dependency graph and affects resolution. - Detecting the correct package name for python packages in nixpkgs often failed since the attribute names don't follow a fixed schema. This lead to a handful of different errors in different situations. Now the package names are extracted from the pypi `url` inside the `src` attribute which is much more reliable. For packages which are not fetched from pypi, the `pname` attribute is used as fallback. - Fixed bug which lead to the error `attribute 'sdist' missing` if a package from the nixpkgs provider was used which doesn't publish it's source on pypi. (For example `tensorflow`) ### Other Changes - Mach-nix now uses a revision of the nixpkgs-unstable branch instead of nixos-20.03 as base fo the tool and the nixpkgs provider. - Updated revision of the dependency DB
2020-08-09 13:24:12 +00:00
def context(py_ver: PyVer, platform: str, system: str):
2020-04-22 09:28:58 +00:00
context = DEFAULT_CONTEXT.copy()
context.update(dict(
version 2.2.0 (#60) # 2.2.0 (09 Aug 2020) Improved success rate, MacOS support, bugfixes, optimizations ### Features - Improved selection of wheel releases. MacOS is now supported and architectures besides x86_64 should be handled correctly. - Whenever mach-nix resolves dependencies, a visualization of the resulting dependency tree is printed on the terminal. - The dependency DB is now accessed through a caching layer which reduces the resolver's CPU time significantly for larger environments. - The python platform context is now generated from the nix build environment variable `system`. This should decrease the chance of impurities during dependency resolution. ### Fixes - The requires_python attribute of wheels was not respected. This lead to failing builds especially for older python versions. Now `requires_python` is part of the dependency graph and affects resolution. - Detecting the correct package name for python packages in nixpkgs often failed since the attribute names don't follow a fixed schema. This lead to a handful of different errors in different situations. Now the package names are extracted from the pypi `url` inside the `src` attribute which is much more reliable. For packages which are not fetched from pypi, the `pname` attribute is used as fallback. - Fixed bug which lead to the error `attribute 'sdist' missing` if a package from the nixpkgs provider was used which doesn't publish it's source on pypi. (For example `tensorflow`) ### Other Changes - Mach-nix now uses a revision of the nixpkgs-unstable branch instead of nixos-20.03 as base fo the tool and the nixpkgs provider. - Updated revision of the dependency DB
2020-08-09 13:24:12 +00:00
platform_version='', # remove impure platform_version
platform_release='', # remove impure kernel verison
platform_system=system[0].upper() + system[1:], # eg. Linux or Darwin
platform_machine=platform, # eg. x86_64
2020-04-22 09:28:58 +00:00
python_version=py_ver.python_version(),
python_full_version=py_ver.python_full_version()
2020-04-22 09:28:58 +00:00
))
return context
class Requirement:
def __init__(self, name, extras, specs: Tuple[Tuple[Tuple[str, str]]], build=None, marker=None):
self.name = name.lower().replace('_', '-')
self.extras = extras or tuple()
self.specs = specs or tuple()
self.build = build
self.marker = marker
def __repr__(self):
return ' '.join(map(lambda x: str(x), filter(lambda e: e, (self.name, self.extras, self.specs, self.build, self.marker))))
@property
def key(self):
return self.name
def __hash__(self):
return hash((self.name, self.specs, self.build))
def filter_reqs_by_eval_marker(reqs: Iterable[Requirement], context: dict, selected_extras=None):
2020-04-22 09:28:58 +00:00
# filter requirements relevant for current environment
for req in reqs:
if req.marker is None:
2020-04-22 09:28:58 +00:00
yield req
elif selected_extras:
for extra in selected_extras:
extra_context = context.copy()
extra_context['extra'] = extra
if distlib.markers.interpret(str(req.marker), extra_context):
yield req
else:
if distlib.markers.interpret(str(req.marker), context):
yield req
2020-04-22 09:28:58 +00:00
2020-11-17 11:29:44 +00:00
all_ops = {'==', '!=', '<=', '>=', '<', '>', '~=', ';'}
2020-10-25 18:09:37 +00:00
@cached(lambda args: tuple(args[0]) if isinstance(args[0], list) else args[0])
2020-04-22 09:28:58 +00:00
def parse_reqs(strs):
2020-10-25 18:09:37 +00:00
lines = iter(pkg_resources.yield_lines(strs))
for line in lines:
if ' #' in line:
line = line[:line.find(' #')]
if line.endswith('\\'):
line = line[:-2].strip()
try:
line += next(lines)
except StopIteration:
return
2020-11-17 11:29:44 +00:00
yield Requirement(*parse_reqs_line(line))
2020-10-25 18:09:37 +00:00
re_specs = re.compile(r"(==|!=|>=|<=|>|<|~=)(.*)")
2020-11-20 18:23:44 +00:00
def parse_spec_part(part):
specs = []
2020-11-20 18:23:44 +00:00
op, ver = re.fullmatch(re_specs, part.strip()).groups()
ver = ver.strip()
specs.append((op, ver))
return list(specs)
2021-06-06 07:57:04 +00:00
extra_name = r"([a-z]|[A-Z]|-|_|\.|\d)+"
re_marker_extras = re.compile(rf"extra *== *'?({extra_name})'?")
def extras_from_marker(marker):
matches = re.findall(re_marker_extras, marker)
if matches:
return tuple(group[0] for group in matches)
return tuple()
re_reqs = re.compile(
2021-06-09 06:10:33 +00:00
r"^(?P<name>([a-z]|[A-Z]|-|_|\d|\.)+)"
rf"(?P<extras>\[({extra_name},?)+\])?"
r"("
2021-06-09 06:10:33 +00:00
r"("
# conda style single spec + single build
r" *(?P<specs_1>(\w|\.|\*|-|!|\+)+)"
r" +(?P<build_1>([a-z]|\d|_|\*|\.)+)"
r"|"
# multiple specs
r" *\(?(?P<specs_0>"
r"\*"
r"|([,\|]? *(==|!=|>=|<=|>|<|~=|=)? *(\* |dev|-?\w?\d(\w|\.|\*|-|\||!|\+)*))+(?![_\d]))\)?"
r"(?P<build_0> *([a-z]|\d|_|\*|\.)+)?"
r"|"
# single spec only
r" *(?P<specs_2>(\w|\.|\*|-|!|\+)+)"
r")"
r")?"
2021-06-09 06:10:33 +00:00
r"( *[:;] *(?P<marker>.*))?$") # marker
2020-11-17 11:29:44 +00:00
def parse_reqs_line(line):
2021-06-09 06:10:33 +00:00
line = line.split("#")[0].strip()
2021-06-06 07:57:04 +00:00
if line.endswith("==*"):
line = line[:-3]
# remove ' symbols
split = line.split(';')
if len(split) > 1:
init, marker = split
2021-06-09 06:10:33 +00:00
init = init.replace("'", "").replace('"', '')
2021-06-06 07:57:04 +00:00
line = init + ';' + marker
else:
2021-06-09 06:10:33 +00:00
line = line.replace("'", "").replace('"', '')
2021-06-06 07:57:04 +00:00
match = re.fullmatch(re_reqs, line)
if not match:
raise Exception(f"couldn't parse: '{line}'")
2021-06-09 06:10:33 +00:00
# groups = list(match.groups())
name = match.group('name')
2021-06-09 06:10:33 +00:00
extras = match.group('extras')
if extras:
extras = tuple(extras.strip('[]').split(','))
else:
extras = tuple()
2021-06-09 06:10:33 +00:00
# extract specs and build
for i in range(2):
try:
all_specs = match.group(f'specs_{i}')
build = match.group(f'build_{i}')
if all_specs is not None:
break
except IndexError:
pass
else:
all_specs = match.group('specs_2')
build = None
if build:
build = build.strip()
if all_specs:
2020-11-20 18:23:44 +00:00
all_specs_raw = all_specs.split('|')
all_specs = []
for specs in all_specs_raw:
parts = specs.split(',')
parsed_parts = []
for part in parts:
if not re.search(r"==|!=|>=|<=|>|<|~=|=", part):
part = '==' + part
elif re.fullmatch(r"=\d(\d|\.|\*|[a-z])*", part):
part = '=' + part
parsed_parts += parse_spec_part(part)
all_specs.append(tuple(parsed_parts))
all_specs = tuple(all_specs)
2021-06-09 06:10:33 +00:00
marker = match.group('marker')
if marker:
extras_marker = extras_from_marker(marker)
extras = extras + extras_marker
return name, extras, all_specs, build, marker
@cached(keyfunc=lambda args: hash((tuple(args[0]), args[1])))
def filter_versions(
versions: List[Version],
req: Requirement):
"""
Reduces a given list of versions to contain only versions
which are allowed according to the given specifiers
"""
assert isinstance(versions, list)
versions = list(versions)
if not req.specs:
return versions
all_versions = []
for specs in req.specs:
for op, ver in specs:
if op == '==':
if str(ver) == "*":
return versions
elif '*' in str(ver):
op = '='
ver = parse_ver(ver)
versions = list(filter(lambda v: ver_eval(v, f"{op}{ver}"), versions))
all_versions += list(versions)
2021-10-12 18:12:49 +00:00
return all_versions