[Notes] [Git][BuildStream/buildstream][chandan/pip-source] 2 commits: Add pip source plugin



Title: GitLab

Chandan Singh pushed to branch chandan/pip-source at BuildStream / buildstream

Commits:

16 changed files:

Changes:

  • buildstream/_versions.py
    ... ... @@ -23,7 +23,7 @@
    23 23
     # This version is bumped whenever enhancements are made
    
    24 24
     # to the `project.conf` format or the core element format.
    
    25 25
     #
    
    26
    -BST_FORMAT_VERSION = 11
    
    26
    +BST_FORMAT_VERSION = 12
    
    27 27
     
    
    28 28
     
    
    29 29
     # The base BuildStream artifact version
    

  • buildstream/plugins/sources/pip.py
    1
    +#
    
    2
    +#  Copyright Bloomberg Finance LP
    
    3
    +#
    
    4
    +#  This program is free software; you can redistribute it and/or
    
    5
    +#  modify it under the terms of the GNU Lesser General Public
    
    6
    +#  License as published by the Free Software Foundation; either
    
    7
    +#  version 2 of the License, or (at your option) any later version.
    
    8
    +#
    
    9
    +#  This library is distributed in the hope that it will be useful,
    
    10
    +#  but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    11
    +#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    
    12
    +#  Lesser General Public License for more details.
    
    13
    +#
    
    14
    +#  You should have received a copy of the GNU Lesser General Public
    
    15
    +#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
    
    16
    +#
    
    17
    +#  Authors:
    
    18
    +#        Chandan Singh <csingh43 bloomberg net>
    
    19
    +
    
    20
    +"""
    
    21
    +pip - stage python packages using pip
    
    22
    +=====================================
    
    23
    +
    
    24
    +**Host depndencies:**
    
    25
    +
    
    26
    +  * ``virtualenv`` CLI tool or ``venv`` python module
    
    27
    +
    
    28
    +**Usage:**
    
    29
    +
    
    30
    +.. code:: yaml
    
    31
    +
    
    32
    +   # Specify the pip source kind
    
    33
    +   kind: pip
    
    34
    +
    
    35
    +   # Optionally specify the python executable, defaults to system "python"
    
    36
    +   # Note that either the venv module or the virtualenv CLI tool must be
    
    37
    +   # available
    
    38
    +   python-exe: python3.6
    
    39
    +
    
    40
    +   # Optionally specify index url, defaults to PyPi
    
    41
    +   index-url: https://mypypi.example.com/simple
    
    42
    +
    
    43
    +   # Optionally specify the path to requirements files
    
    44
    +   # Note that either 'requirements-files' or 'packages' must be defined
    
    45
    +   requirements-files:
    
    46
    +   - requirements.txt
    
    47
    +
    
    48
    +   # Optionally specify a list of additional packages
    
    49
    +   # Note that either 'requirements-files' or 'packages' must be defined
    
    50
    +   packages:
    
    51
    +   - flake8
    
    52
    +
    
    53
    +   # Optionally specify a relative staging directory
    
    54
    +   directory: path/to/stage
    
    55
    +
    
    56
    +   # Specify the ref. It is a list of strings of format
    
    57
    +   # `<package-name>==<version>`, one per line.
    
    58
    +   # Usually this will be contents of a requirements.txt file where all
    
    59
    +   # package versions have been frozen.
    
    60
    +   ref: "flake8==3.5.0\\nmccabe==0.6.1\\npkg-resources==0.0.0\\npycodestyle==2.3.1\\npyflakes==1.6.0"
    
    61
    +
    
    62
    +.. note::
    
    63
    +
    
    64
    +   The ``pip`` plugin is available since :ref:`format version 12 <project_format_version>`
    
    65
    +
    
    66
    +"""
    
    67
    +
    
    68
    +import hashlib
    
    69
    +import os
    
    70
    +
    
    71
    +from buildstream import Consistency, Source, SourceError, utils
    
    72
    +
    
    73
    +_PYPI_INDEX_URL = 'https://pypi.org/simple/'
    
    74
    +
    
    75
    +
    
    76
    +class PipSource(Source):
    
    77
    +    # pylint: disable=attribute-defined-outside-init
    
    78
    +
    
    79
    +    # We need access to previous sources at track time to use requirements.txt
    
    80
    +    # but not at fetch time as self.ref should contain sufficient information
    
    81
    +    # for this plugin
    
    82
    +    requires_previous_sources_track = True
    
    83
    +
    
    84
    +    def configure(self, node):
    
    85
    +        self.node_validate(node, ['index-url', 'packages', 'python-exe', 'ref', 'requirements-files'] +
    
    86
    +                           Source.COMMON_CONFIG_KEYS)
    
    87
    +        self.ref = self.node_get_member(node, str, 'ref', None)
    
    88
    +        self.python_exe = self.node_get_member(node, str, 'python-exe', 'python')
    
    89
    +        self.index_url = self.node_get_member(node, str, 'index-url', _PYPI_INDEX_URL)
    
    90
    +        self.packages = self.node_get_member(node, list, 'packages', [])
    
    91
    +        self.requirements_files = self.node_get_member(node, list, 'requirements-files', [])
    
    92
    +
    
    93
    +        if not (self.packages or self.requirements_files):
    
    94
    +            raise SourceError("{}: Either 'packages' and 'requirements-files' must be specified". format(self))
    
    95
    +
    
    96
    +    def preflight(self):
    
    97
    +        # Try to find a way to open virtual environments on the host
    
    98
    +        try:
    
    99
    +            # Look for the virtualenv CLI first
    
    100
    +            venv = utils.get_host_tool('virtualenv')
    
    101
    +            self.venv_cmd = [venv, '--python', self.python_exe]
    
    102
    +        except utils.ProgramNotFoundError:
    
    103
    +            # Fall back to venv module if it is installed
    
    104
    +            python_exe = utils.get_host_tool(self.python_exe)
    
    105
    +            rc = self.call([python_exe, '-m', 'venv', '--help'])
    
    106
    +            if rc == 0:
    
    107
    +                self.venv_cmd = [python_exe, '-m', 'venv']
    
    108
    +            else:
    
    109
    +                raise SourceError("{}: venv module not found using python: {}"
    
    110
    +                                  .format(self, python_exe))
    
    111
    +
    
    112
    +    def get_unique_key(self):
    
    113
    +        return [self.venv_cmd, self.index_url, self.ref]
    
    114
    +
    
    115
    +    def get_consistency(self):
    
    116
    +        if not self.ref:
    
    117
    +            return Consistency.INCONSISTENT
    
    118
    +        # FIXME add a stronger consistency check
    
    119
    +        # Currently we take the presence of "something" as an indication that
    
    120
    +        # we have the right things in cache
    
    121
    +        if os.path.exists(self.mirror) and os.listdir(self.mirror):
    
    122
    +            return Consistency.CACHED
    
    123
    +        return Consistency.RESOLVED
    
    124
    +
    
    125
    +    def get_ref(self):
    
    126
    +        return self.ref
    
    127
    +
    
    128
    +    def load_ref(self, node):
    
    129
    +        self.ref = self.node_get_member(node, str, 'ref', None)
    
    130
    +
    
    131
    +    def set_ref(self, ref, node):
    
    132
    +        node['ref'] = self.ref = ref
    
    133
    +
    
    134
    +    def track(self, previous_sources_dir):
    
    135
    +        # XXX pip does not offer any public API other than the CLI tool so it
    
    136
    +        # is not feasible to correctly parse the requirements file or to check
    
    137
    +        # which package versions pip is going to install.
    
    138
    +        # See https://pip.pypa.io/en/stable/user_guide/#using-pip-from-your-program
    
    139
    +        # for details.
    
    140
    +        # As a result, we have to wastefully install the packages during track.
    
    141
    +        with self.tempdir() as tmpdir:
    
    142
    +            pip = self._venv_pip(tmpdir)
    
    143
    +
    
    144
    +            install_args = [pip, 'install', '--index-url', self.index_url]
    
    145
    +            for requirement_file in self.requirements_files:
    
    146
    +                fpath = os.path.join(previous_sources_dir, requirement_file)
    
    147
    +                install_args += ['-r', fpath]
    
    148
    +            install_args += self.packages
    
    149
    +
    
    150
    +            self.call(install_args, fail="Failed to install python packages")
    
    151
    +            _, reqs = self.check_output([pip, 'freeze'])
    
    152
    +
    
    153
    +        return reqs.strip()
    
    154
    +
    
    155
    +    def fetch(self):
    
    156
    +        with self.tempdir() as tmpdir:
    
    157
    +            pip = self._venv_pip(tmpdir)
    
    158
    +            packages = self.ref.strip().split('\n')
    
    159
    +            self.call([pip, 'install',
    
    160
    +                       '--index-url', self.index_url,
    
    161
    +                       '--prefix', self.mirror] +
    
    162
    +                      packages,
    
    163
    +                      fail="Failed to install python packages: {}".format(packages))
    
    164
    +
    
    165
    +    def stage(self, directory):
    
    166
    +        with self.timed_activity("Staging Python packages", silent_nested=True):
    
    167
    +            utils.copy_files(self.mirror, directory)
    
    168
    +
    
    169
    +    @property
    
    170
    +    def mirror(self):
    
    171
    +        """Directory where this source should stage its files
    
    172
    +
    
    173
    +        """
    
    174
    +        path = os.path.join(self.get_mirror_directory(),
    
    175
    +                            self.index_url,
    
    176
    +                            hashlib.sha256(self.ref.encode()).hexdigest())
    
    177
    +        os.makedirs(path, exist_ok=True)
    
    178
    +        return path
    
    179
    +
    
    180
    +    def _venv_pip(self, directory):
    
    181
    +        """Open a virtual environment in given directory and return pip path
    
    182
    +
    
    183
    +        """
    
    184
    +        self.call(self.venv_cmd + [directory], fail="Failed to initialize virtual environment")
    
    185
    +        pip_exe = os.path.join(directory, 'bin', 'pip')
    
    186
    +        if not os.path.isfile(pip_exe):
    
    187
    +            raise SourceError("Failed to initialize virtual environment")
    
    188
    +        return pip_exe
    
    189
    +
    
    190
    +
    
    191
    +def setup():
    
    192
    +    return PipSource

  • doc/source/core_plugins.rst
    ... ... @@ -58,6 +58,7 @@ Sources
    58 58
        sources/ostree
    
    59 59
        sources/patch
    
    60 60
        sources/deb
    
    61
    +   sources/pip
    
    61 62
     
    
    62 63
     
    
    63 64
     External plugins
    

  • tests/integration/pip.pytests/integration/pip_element.py

  • tests/integration/pip_source.py
    1
    +import os
    
    2
    +import pytest
    
    3
    +
    
    4
    +from buildstream import _yaml
    
    5
    +
    
    6
    +from tests.testutils import cli_integration as cli
    
    7
    +from tests.testutils.integration import assert_contains
    
    8
    +
    
    9
    +
    
    10
    +pytestmark = pytest.mark.integration
    
    11
    +
    
    12
    +
    
    13
    +DATA_DIR = os.path.join(
    
    14
    +    os.path.dirname(os.path.realpath(__file__)),
    
    15
    +    "project"
    
    16
    +)
    
    17
    +
    
    18
    +
    
    19
    +@pytest.mark.datafiles(DATA_DIR)
    
    20
    +def test_pip_source(cli, tmpdir, datafiles):
    
    21
    +    project = os.path.join(datafiles.dirname, datafiles.basename)
    
    22
    +    checkout = os.path.join(cli.directory, 'checkout')
    
    23
    +    element_path = os.path.join(project, 'elements')
    
    24
    +    element_name = 'pip/hello.bst'
    
    25
    +
    
    26
    +    element = {
    
    27
    +        'kind': 'import',
    
    28
    +        'sources': [
    
    29
    +            {
    
    30
    +                'kind': 'local',
    
    31
    +                'path': 'files/pip-source'
    
    32
    +            },
    
    33
    +            {
    
    34
    +                'kind': 'pip',
    
    35
    +                'python-exe': 'python3',
    
    36
    +                'index-url': 'file://{}'.format(os.path.realpath(os.path.join(project, 'files', 'pypi-repo'))),
    
    37
    +                'requirements-files': ['myreqs.txt'],
    
    38
    +                'packages': ['app2']
    
    39
    +            }
    
    40
    +        ]
    
    41
    +    }
    
    42
    +    os.makedirs(os.path.dirname(os.path.join(element_path, element_name)), exist_ok=True)
    
    43
    +    _yaml.dump(element, os.path.join(element_path, element_name))
    
    44
    +
    
    45
    +    result = cli.run(project=project, args=['track', element_name])
    
    46
    +    assert result.exit_code == 0
    
    47
    +
    
    48
    +    result = cli.run(project=project, args=['build', element_name])
    
    49
    +    assert result.exit_code == 0
    
    50
    +
    
    51
    +    result = cli.run(project=project, args=['checkout', element_name, checkout])
    
    52
    +    assert result.exit_code == 0
    
    53
    +
    
    54
    +    assert_contains(checkout, ['/bin', '/bin/app1', '/bin/app2'])

  • tests/integration/project/files/pip-source/myreqs.txt
    1
    +app1

  • tests/integration/project/files/pypi-repo/app1/App1-0.1.tar.gz
    No preview for this file type
  • tests/integration/project/files/pypi-repo/app1/index.html
    1
    +<html>
    
    2
    +  <head>
    
    3
    +    <title>Links for app1</title>
    
    4
    +  </head>
    
    5
    +  <body>
    
    6
    +    <a href="">'App1-0.1.tar.gz'>App1-0.1.tar.gz</a><br />
    
    7
    +  </body>
    
    8
    +</html>

  • tests/integration/project/files/pypi-repo/app2/App2-0.1.tar.gz
    No preview for this file type
  • tests/integration/project/files/pypi-repo/app2/index.html
    1
    +<html>
    
    2
    +  <head>
    
    3
    +    <title>Links for app1</title>
    
    4
    +  </head>
    
    5
    +  <body>
    
    6
    +    <a href="">'App2-0.1.tar.gz'>App2-0.1.tar.gz</a><br />
    
    7
    +  </body>
    
    8
    +</html>

  • tests/sources/pip.py
    1
    +import os
    
    2
    +import pytest
    
    3
    +
    
    4
    +from buildstream._exceptions import ErrorDomain
    
    5
    +from buildstream import _yaml
    
    6
    +from tests.testutils import cli
    
    7
    +
    
    8
    +DATA_DIR = os.path.join(
    
    9
    +    os.path.dirname(os.path.realpath(__file__)),
    
    10
    +    'pip',
    
    11
    +)
    
    12
    +
    
    13
    +
    
    14
    +def generate_project(project_dir, tmpdir):
    
    15
    +    project_file = os.path.join(project_dir, "project.conf")
    
    16
    +    _yaml.dump({'name': 'foo'}, project_file)
    
    17
    +
    
    18
    +
    
    19
    +# Test that without ref, consistency is set appropriately.
    
    20
    +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'no-ref'))
    
    21
    +def test_no_ref(cli, tmpdir, datafiles):
    
    22
    +    project = os.path.join(datafiles.dirname, datafiles.basename)
    
    23
    +    generate_project(project, tmpdir)
    
    24
    +    assert cli.get_element_state(project, 'target.bst') == 'no reference'
    
    25
    +
    
    26
    +
    
    27
    +# Test that pip is not allowed to be the first source
    
    28
    +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'first-source-pip'))
    
    29
    +def test_first_source(cli, tmpdir, datafiles):
    
    30
    +    project = os.path.join(datafiles.dirname, datafiles.basename)
    
    31
    +    generate_project(project, tmpdir)
    
    32
    +    result = cli.run(project=project, args=[
    
    33
    +        'show', 'target.bst'
    
    34
    +    ])
    
    35
    +    result.assert_main_error(ErrorDomain.ELEMENT, None)
    
    36
    +
    
    37
    +
    
    38
    +# Test that error is raised when neither packges nor requirements files
    
    39
    +# have been specified
    
    40
    +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'no-packages'))
    
    41
    +def test_no_packages(cli, tmpdir, datafiles):
    
    42
    +    project = os.path.join(datafiles.dirname, datafiles.basename)
    
    43
    +    generate_project(project, tmpdir)
    
    44
    +    result = cli.run(project=project, args=[
    
    45
    +        'show', 'target.bst'
    
    46
    +    ])
    
    47
    +    result.assert_main_error(ErrorDomain.SOURCE, None)

  • tests/sources/pip/first-source-pip/target.bst
    1
    +kind: import
    
    2
    +description: pip should not be allowed to be the first source
    
    3
    +sources:
    
    4
    +- kind: pip
    
    5
    +  python-exe: python3
    
    6
    +  packages:
    
    7
    +  - flake8

  • tests/sources/pip/no-packages/file
    1
    +Hello World!

  • tests/sources/pip/no-packages/target.bst
    1
    +kind: import
    
    2
    +description: The kind of this element is irrelevant.
    
    3
    +sources:
    
    4
    +- kind: local
    
    5
    +  path: file
    
    6
    +- kind: pip
    
    7
    +  python-exe: python3

  • tests/sources/pip/no-ref/file
    1
    +Hello World!

  • tests/sources/pip/no-ref/target.bst
    1
    +kind: import
    
    2
    +description: The kind of this element is irrelevant.
    
    3
    +sources:
    
    4
    +- kind: local
    
    5
    +  path: file
    
    6
    +- kind: pip
    
    7
    +  python-exe: python3
    
    8
    +  packages:
    
    9
    +  - flake8



  • [Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]