Skip to content

Commit

Permalink
Merge branch 'check'
Browse files Browse the repository at this point in the history
* check:
  New command "prequ check"
  Implement silent flag for build-wheels
  Demand wheel lines to use == version specifier
  Move requirement.in generation to PreRequirements
  PreRequirements: Add some documentation
  Use term requirement set in pre-requirements
  Replace pre-requirement "mode" with "label" in terms
  • Loading branch information
suutari-ai committed Mar 12, 2017
2 parents 27f7d3c + b76d169 commit 32c5e60
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 74 deletions.
2 changes: 2 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Unreleased
- Fix constraint handling: Do not add new dependencies from constraints
- compile-in: Rename "--no-trusted-host" to "--no-emit-trusted-host"
- Remove dependency on the "first" Python package
- Demand using equality operator (==) in lines with a wheel instruction
- Add new command "prequ check" for checking generated requirements

0.300.0
-------
Expand Down
8 changes: 8 additions & 0 deletions prequ/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ def __init__(self, ireq_a, ireq_b):
def __str__(self):
message = "Incompatible requirements found: {} and {}"
return message.format(self.ireq_a, self.ireq_b)


class FileOutdated(PrequError):
pass


class WheelMissing(PrequError):
pass
103 changes: 76 additions & 27 deletions prequ/prereqfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@


class PreRequirements(object):
"""
Pre-requirements specification.
Pre-requirements define how requirements files should be generated:
various options, package names and versions.
It is possible to have several requirement sets defined. A single
requirements file will be generated from each set. These sets are
addressed by a label which determines the output file name. Label
"base" is the default and its output is "requirements.txt". Other
output files are named "requirements-{label}.txt" by their labels.
"""

fields = [
('options.annotate', bool),
('options.generate_hashes', bool),
Expand All @@ -40,6 +53,19 @@ class PreRequirements(object):

@classmethod
def from_directory(cls, directory):
"""
Get pre-requirements of a directory.
Reads the first existing pre-requirements configuration file(s)
and parses it/them to a PreRequirements object. Supported
configuration files in preference order are:
* setup.cfg, [prequ] section
* requirements.pre
* requirements.in and requirements-*.in
:rtype: PreRequirements
"""
def path(filename):
return os.path.join(directory, filename)

Expand Down Expand Up @@ -99,14 +125,14 @@ def from_in_files(cls, *filenames):
for filepath in filenames:
fn = os.path.basename(filepath)
if fn == 'requirements.in':
mode = 'base'
label = 'base'
elif fn.startswith('requirements-') and fn.endswith('.in'):
mode = fn.split('requirements-', 1)[1].rsplit('.in', 1)[0]
label = fn.split('requirements-', 1)[1].rsplit('.in', 1)[0]
else:
raise InvalidPreRequirements(
'Invalid in-file name: {}'.format(fn))
with io.open(filepath, 'rt', encoding='utf-8') as fp:
reqs[mode] = fp.read()
reqs[label] = fp.read()
return cls.from_dict({'requirements': reqs})

@classmethod
Expand All @@ -116,16 +142,17 @@ def from_dict(cls, conf_data):
raise InvalidPreRequirements(
'Errors in pre-requirement data: {}'.format(', '.join(errors)))

(requirements, extra_opts) = parse_reqs(conf_data['requirements'])
input_reqs = conf_data['requirements']
(requirement_sets, extra_opts) = parse_input_requirements(input_reqs)
options = conf_data.get('options', {})
options.update(extra_opts)
return cls(requirements, **options)
return cls(requirement_sets, **options)

def __init__(self, requirements, **kwargs):
assert isinstance(requirements, dict)
assert all(isinstance(x, text) for x in requirements.values())
def __init__(self, requirement_sets, **kwargs):
assert isinstance(requirement_sets, dict)
assert all(isinstance(x, text) for x in requirement_sets.values())

self.requirements = requirements
self.requirement_sets = requirement_sets
self.annotate = kwargs.pop('annotate', False)
self.generate_hashes = kwargs.pop('generate_hashes', False)
self.header = kwargs.pop('header', True)
Expand All @@ -140,14 +167,35 @@ def __init__(self, requirements, **kwargs):
#: List of wheels to build, format [(wheel_src_name, pkg, ver)]
self.wheels_to_build = kwargs.pop('wheels_to_build', [])

def get_requirements(self):
base_req = self.requirements.get('base')
base_reqs = [('base', base_req)] if base_req is not None else []
non_base_reqs = [
(mode, self.requirements[mode])
for mode in self.requirements
if mode != 'base']
return base_reqs + non_base_reqs
@property
def labels(self):
def sort_key(label):
return (0, label) if label == 'base' else (1, label)
return sorted(self.requirement_sets.keys(), key=sort_key)

def get_output_file_for(self, label):
"""
Get output file name for a requirement set.
:type label: text
:rtype: text
"""
return (
'requirements.txt' if label == 'base' else
'requirements-{}.txt'.format(label))

def get_requirements_in_for(self, label):
"""
Get requirements.in file content for a requirement set.
:type label: text
:rtype: text
"""
constraint_line = (
'-c {}\n'.format(self.get_output_file_for('base'))
if label != 'base' and 'base' in self.requirement_sets
else '')
return constraint_line + self.requirement_sets[label]

def get_wheels_to_build(self):
for (wheel_src_name, pkg, ver) in self.wheels_to_build:
Expand Down Expand Up @@ -268,14 +316,14 @@ def _get_type_error(value, typespec, fieldspec):
return 'Field "{}" should be {}'.format(fieldspec, typename)


def parse_reqs(requirements_map):
def parse_input_requirements(input_requirements):
extra_opts = {}
reqs = {}
for (mode, req_data) in requirements_map.items():
(req, opts) = _parse_req_data(req_data)
requirement_sets = {}
for (label, req_data) in input_requirements.items():
(requirement_set, opts) = _parse_req_data(req_data)
_merge_update_dict(extra_opts, opts)
reqs[mode] = req
return (reqs, extra_opts)
requirement_sets[label] = requirement_set
return (requirement_sets, extra_opts)


def _parse_req_data(req_data):
Expand All @@ -284,7 +332,8 @@ def _parse_req_data(req_data):
for line in req_data.splitlines():
match = WHEEL_LINE_RX.match(line)
if match:
(wheel_data, req_line) = _parse_wheel_match(**match.groupdict())
(wheel_data, req_line) = _parse_wheel_match(
line, **match.groupdict())
wheels_to_build.append(wheel_data)
result_lines.append(req_line)
else:
Expand All @@ -297,10 +346,10 @@ def _parse_req_data(req_data):
'\(\s*wheel from \s*(?P<wheel_src_name>\S+)\)$')


def _parse_wheel_match(pkg, verspec, wheel_src_name):
if not verspec.startswith(('>=', '~=', '==')):
def _parse_wheel_match(line, pkg, verspec, wheel_src_name):
if not verspec.startswith('=='):
raise InvalidPreRequirements(
'Wheel needs version specifier (==, ~=, or >=): {}'.format(pkg))
'Wheel lines must use "==" version specifier: {}'.format(line))
ver = verspec[2:]
wheel_data = (wheel_src_name, pkg, ver)
req_line = pkg + verspec
Expand Down
37 changes: 27 additions & 10 deletions prequ/scripts/build_wheels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,55 @@

import click

from ..exceptions import WheelMissing, PrequError
from ..logging import log
from ..prereqfile import PreRequirements

click.disable_unicode_literals_warning = True


@click.command()
def main():
@click.option('-s', '--silent', is_flag=True, help="Show no output")
@click.option('-c', '--check', is_flag=True,
help="Check if the wheels exists")
def main(silent, check):
"""
Build wheels of required packages.
"""
build_wheels()
try:
build_wheels(silent=silent, check_only=check)
except PrequError as error:
log.error('{}'.format(error))
raise SystemExit(1)


def build_wheels():
def build_wheels(silent=False, check_only=False):
prereq = PreRequirements.from_directory('.')
to_build = list(prereq.get_wheels_to_build())
for (pkg, ver, url) in to_build:
build_wheel(prereq, pkg, ver, url)
build_wheel(prereq, pkg, ver, url, silent, check_only)


def build_wheel(prereq, pkg, ver, url):
def build_wheel(prereq, pkg, ver, url, silent=False, check_only=False):
info = log.info if not silent else (lambda x: None)
already_built = get_wheels(prereq, pkg, ver)
if check_only:
if already_built:
info('{} exists'.format(already_built[0]))
return
raise WheelMissing('Wheel for {} {} is missing'.format(pkg, ver))
if already_built:
print('*** Already built: {}'.format(already_built[0]))
info('*** Already built: {}'.format(already_built[0]))
return
print('*** Building wheel for {} {} from {}'.format(pkg, ver, url))
call('pip wheel -v -w {w} --no-deps {u}', w=prereq.wheel_dir, u=url)
info('*** Building wheel for {} {} from {}'.format(pkg, ver, url))
call('pip wheel {verbosity} -w {w} --no-deps {u}',
verbosity=('-q' if silent else '-v'),
w=prereq.wheel_dir, u=url)
built_wheel = get_wheels(prereq, pkg, ver)[0]
print('*** Built: {}'.format(built_wheel))
info('*** Built: {}'.format(built_wheel))
for wheel in get_wheels(prereq, pkg): # All versions
if wheel != built_wheel:
print('*** Removing: {}'.format(wheel))
info('*** Removing: {}'.format(wheel))
os.remove(wheel)


Expand Down
15 changes: 15 additions & 0 deletions prequ/scripts/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import click

from . import build_wheels
from . import compile


@click.command()
@click.option('-s', '--silent', is_flag=True, help="Show no output")
@click.pass_context
def main(ctx, silent):
"""
Check if generated requirements are up-to-date.
"""
ctx.invoke(build_wheels.main, check=True, silent=silent)
ctx.invoke(compile.main, check=True, silent=silent)
Loading

0 comments on commit 32c5e60

Please sign in to comment.