[Notes] [Git][BuildStream/buildstream][lachlan/pickle-yaml-test-list-composite] 20 commits: bst-docker-import: Consistently use stderr for all logs



Title: GitLab

Lachlan pushed to branch lachlan/pickle-yaml-test-list-composite at BuildStream / buildstream

Commits:

9 changed files:

Changes:

  • CONTRIBUTING.rst
    ... ... @@ -14,7 +14,7 @@ if no issue already exists.
    14 14
     
    
    15 15
     For policies on how to submit an issue and how to use our project labels,
    
    16 16
     we recommend that you read the `policies guide
    
    17
    -<https://gitlab.com/BuildStream/nosoftware/alignment/blob/master/BuildStream_policies.md>`_
    
    17
    +<https://gitlab.com/BuildStream/nosoftware/alignment/blob/master/BuildStream_policies.md>`_.
    
    18 18
     
    
    19 19
     
    
    20 20
     .. _contributing_fixing_bugs:
    
    ... ... @@ -270,6 +270,11 @@ comments and docstrings.
    270 270
         *Since: 1.2*
    
    271 271
         """
    
    272 272
     
    
    273
    +.. note::
    
    274
    +
    
    275
    +   Python does not support docstrings on instance variables, but sphinx does
    
    276
    +   pick them up and includes them in the generated documentation.
    
    277
    +
    
    273 278
     **Internal instance variable**::
    
    274 279
     
    
    275 280
       def __init__(self, context, element):
    
    ... ... @@ -404,7 +409,7 @@ on a Python class in BuildStream::
    404 409
              # Implementation of the "frobbish" abstract method
    
    405 410
              # defined by the parent Bar class.
    
    406 411
              #
    
    407
    -	 return True
    
    412
    +         return True
    
    408 413
     
    
    409 414
           ################################################
    
    410 415
           #                 Public Methods               #
    
    ... ... @@ -445,7 +450,7 @@ on a Python class in BuildStream::
    445 450
           # Returns:
    
    446 451
           #    (int): The count of this foo
    
    447 452
           #
    
    448
    -      def set_count(self, count):
    
    453
    +      def get_count(self, count):
    
    449 454
     
    
    450 455
               return self._count
    
    451 456
     
    
    ... ... @@ -459,7 +464,7 @@ on a Python class in BuildStream::
    459 464
           #       Even though these are private implementation
    
    460 465
           #       details, they still MUST have API documenting
    
    461 466
           #       comments on them.
    
    462
    -      
    
    467
    +
    
    463 468
           # _do_frobbing()
    
    464 469
           #
    
    465 470
           # Does the actual frobbing
    
    ... ... @@ -494,10 +499,10 @@ reference on how the PEP-8 defines public and non-public.
    494 499
     
    
    495 500
       A private symbol must be denoted by a leading underscore.
    
    496 501
     
    
    497
    -* When a class can have subclasses (for example, the ``Sandbox`` or ``Platform``
    
    502
    +* When a class can have subclasses, then private symbols should be denoted
    
    503
    +  by two leading underscores. For example, the ``Sandbox`` or ``Platform``
    
    498 504
       classes which have various implementations, or the ``Element`` and ``Source``
    
    499
    -  classes which plugins derive from), then private symbols should be denoted
    
    500
    -  by two leading underscores.
    
    505
    +  classes which plugins derive from.
    
    501 506
     
    
    502 507
       The double leading underscore naming convention invokes Python's name
    
    503 508
       mangling algorithm which helps prevent namespace collisions in the case
    
    ... ... @@ -536,7 +541,7 @@ In order to disambiguate between:
    536 541
     
    
    537 542
     * Symbols which are publicly accessible details of the ``Element`` class, can
    
    538 543
       be accessed by BuildStream internals, but must remain hidden from the
    
    539
    -  *"Public API Surface"*
    
    544
    +  *"Public API Surface"*.
    
    540 545
     
    
    541 546
     * Symbols which are private to the ``Element`` class, and cannot be accessed
    
    542 547
       from outside of the ``Element`` class at all.
    
    ... ... @@ -586,7 +591,7 @@ is found at ``_artifactcache/artifactcache.py``.
    586 591
     
    
    587 592
     Imports
    
    588 593
     ~~~~~~~
    
    589
    -Module imports inside BuildStream are done with relative ``.`` notation
    
    594
    +Module imports inside BuildStream are done with relative ``.`` notation:
    
    590 595
     
    
    591 596
     **Good**::
    
    592 597
     
    
    ... ... @@ -628,12 +633,12 @@ which exposes an instance variable is the only one in control of the value of th
    628 633
     variable.
    
    629 634
     
    
    630 635
     * If an instance variable is public and must be modified; then it must be
    
    631
    -  modified using a :ref:`mutator <contributing_accessor_mutator>`
    
    636
    +  modified using a :ref:`mutator <contributing_accessor_mutator>`.
    
    632 637
     
    
    633 638
     * Ideally for better encapsulation, all object state is declared as
    
    634 639
       :ref:`private instance variables <contributing_public_and_private>` and can
    
    635 640
       only be accessed by external classes via public :ref:`accessors and mutators
    
    636
    -  <contributing_accessor_mutator>`
    
    641
    +  <contributing_accessor_mutator>`.
    
    637 642
     
    
    638 643
     .. note::
    
    639 644
     
    
    ... ... @@ -720,10 +725,10 @@ In BuildStream, we use the term *"Abstract Method"*, to refer to
    720 725
     a method which **can** be overridden by a subclass, whereas it
    
    721 726
     is **illegal** to override any other method.
    
    722 727
     
    
    723
    -* Abstract methods are allowed to have default implementations
    
    728
    +* Abstract methods are allowed to have default implementations.
    
    724 729
     
    
    725 730
     * Subclasses are not allowed to redefine the calling signature
    
    726
    -  of an abstract method, or redefine the API contract in any way
    
    731
    +  of an abstract method, or redefine the API contract in any way.
    
    727 732
     
    
    728 733
     * Subclasses are not allowed to override any other methods.
    
    729 734
     
    
    ... ... @@ -798,7 +803,7 @@ BstError parameters
    798 803
     When raising ``BstError`` class exceptions, there are some common properties
    
    799 804
     which can be useful to know about:
    
    800 805
     
    
    801
    -* **message:** The brief human readable error, will be formatted on one line in the frontend
    
    806
    +* **message:** The brief human readable error, will be formatted on one line in the frontend.
    
    802 807
     
    
    803 808
     * **detail:** An optional detailed human readable message to accompany the **message** summary
    
    804 809
       of the error. This is often used to recommend the user some course of action, or to provide
    
    ... ... @@ -974,9 +979,9 @@ symbols to a minimum, this is important for both
    974 979
     
    
    975 980
     When anyone visits a file, there are two levels of comprehension:
    
    976 981
     
    
    977
    -* What do I need to know in order to *use* this object
    
    982
    +* What do I need to know in order to *use* this object.
    
    978 983
     
    
    979
    -* What do I need to know in order to *modify* this object
    
    984
    +* What do I need to know in order to *modify* this object.
    
    980 985
     
    
    981 986
     For the former, we want the reader to understand with as little effort
    
    982 987
     as possible, what the public API contract is for a given object and consequently,
    
    ... ... @@ -1001,9 +1006,9 @@ well documented and minimal.
    1001 1006
     
    
    1002 1007
     When adding new API to a given object for a new purpose, consider whether
    
    1003 1008
     the new API is in any way redundant with other API (should this value now
    
    1004
    -go into the constructor, since we use it more than once ? could this
    
    1009
    +go into the constructor, since we use it more than once? could this
    
    1005 1010
     value be passed along with another function, and the other function renamed,
    
    1006
    -to better suit the new purposes of this module/object ?) and repurpose
    
    1011
    +to better suit the new purposes of this module/object?) and repurpose
    
    1007 1012
     the outward facing API of an object as a whole every time.
    
    1008 1013
     
    
    1009 1014
     
    
    ... ... @@ -1183,7 +1188,7 @@ The BuildStream documentation style is as follows:
    1183 1188
     * Cross references should be of the form ``:role:`target```.
    
    1184 1189
     
    
    1185 1190
       * Explicit anchors can be declared as ``.. _anchor_name:`` on a line by itself.
    
    1186
    -  
    
    1191
    +
    
    1187 1192
       * To cross reference arbitrary locations with, for example, the anchor ``anchor_name``,
    
    1188 1193
         always provide some explicit text in the link instead of deriving the text from
    
    1189 1194
         the target, e.g.: ``:ref:`Link text <anchor_name>```.
    
    ... ... @@ -1266,23 +1271,23 @@ Documentation Examples
    1266 1271
     The examples section of the documentation contains a series of standalone
    
    1267 1272
     examples, here are the criteria for an example addition.
    
    1268 1273
     
    
    1269
    -* The example has a ``${name}``
    
    1274
    +* The example has a ``${name}``.
    
    1270 1275
     
    
    1271
    -* The example has a project users can copy and use
    
    1276
    +* The example has a project users can copy and use.
    
    1272 1277
     
    
    1273
    -  * This project is added in the directory ``doc/examples/${name}``
    
    1278
    +  * This project is added in the directory ``doc/examples/${name}``.
    
    1274 1279
     
    
    1275
    -* The example has a documentation component
    
    1280
    +* The example has a documentation component.
    
    1276 1281
     
    
    1277 1282
       * This is added at ``doc/source/examples/${name}.rst``
    
    1278 1283
       * A reference to ``examples/${name}`` is added to the toctree in ``doc/source/examples.rst``
    
    1279 1284
       * This documentation discusses the project elements declared in the project and may
    
    1280
    -    provide some BuildStream command examples
    
    1281
    -  * This documentation links out to the reference manual at every opportunity
    
    1285
    +    provide some BuildStream command examples.
    
    1286
    +  * This documentation links out to the reference manual at every opportunity.
    
    1282 1287
     
    
    1283
    -* The example has a CI test component
    
    1288
    +* The example has a CI test component.
    
    1284 1289
     
    
    1285
    -  * This is an integration test added at ``tests/examples/${name}``
    
    1290
    +  * This is an integration test added at ``tests/examples/${name}``.
    
    1286 1291
       * This test runs BuildStream in the ways described in the example
    
    1287 1292
         and assert that we get the results which we advertize to users in
    
    1288 1293
         the said examples.
    
    ... ... @@ -1309,17 +1314,17 @@ The ``.run`` file format is just another YAML dictionary which consists of a
    1309 1314
     
    
    1310 1315
     Each *command* is a dictionary, the members of which are listed here:
    
    1311 1316
     
    
    1312
    -* ``directory``: The input file relative project directory
    
    1317
    +* ``directory``: The input file relative project directory.
    
    1313 1318
     
    
    1314
    -* ``output``: The input file relative output html file to generate (optional)
    
    1319
    +* ``output``: The input file relative output html file to generate (optional).
    
    1315 1320
     
    
    1316 1321
     * ``fake-output``: Don't really run the command, just pretend to and pretend
    
    1317 1322
       this was the output, an empty string will enable this too.
    
    1318 1323
     
    
    1319
    -* ``command``: The command to run, without the leading ``bst``
    
    1324
    +* ``command``: The command to run, without the leading ``bst``.
    
    1320 1325
     
    
    1321 1326
     * ``shell``: Specifying ``True`` indicates that ``command`` should be run as
    
    1322
    -  a shell command from the project directory, instead of a bst command (optional)
    
    1327
    +  a shell command from the project directory, instead of a bst command (optional).
    
    1323 1328
     
    
    1324 1329
     When adding a new ``.run`` file, one should normally also commit the new
    
    1325 1330
     resulting generated ``.html`` file(s) into the ``doc/source/sessions-stored/``
    
    ... ... @@ -1417,7 +1422,7 @@ a subdirectory beside your test in which to store data.
    1417 1422
     When creating a test that needs data, use the datafiles extension
    
    1418 1423
     to decorate your test case (again, examples exist in the existing
    
    1419 1424
     tests for this), documentation on the datafiles extension can
    
    1420
    -be found here: https://pypi.python.org/pypi/pytest-datafiles
    
    1425
    +be found here: https://pypi.python.org/pypi/pytest-datafiles.
    
    1421 1426
     
    
    1422 1427
     Tests that run a sandbox should be decorated with::
    
    1423 1428
     
    

  • buildstream/_loader/loadelement.py
    ... ... @@ -185,6 +185,6 @@ def _extract_depends_from_node(node, *, key=None):
    185 185
             output_deps.append(dependency)
    
    186 186
     
    
    187 187
         # Now delete the field, we dont want it anymore
    
    188
    -    del node[key]
    
    188
    +    node.pop(key, None)
    
    189 189
     
    
    190 190
         return output_deps

  • buildstream/_loader/loader.py
    ... ... @@ -29,6 +29,7 @@ from .. import _yaml
    29 29
     from ..element import Element
    
    30 30
     from .._profile import Topics, profile_start, profile_end
    
    31 31
     from .._includes import Includes
    
    32
    +from .._yamlcache import YamlCache
    
    32 33
     
    
    33 34
     from .types import Symbol, Dependency
    
    34 35
     from .loadelement import LoadElement
    
    ... ... @@ -112,7 +113,14 @@ class Loader():
    112 113
                 profile_start(Topics.LOAD_PROJECT, target)
    
    113 114
                 junction, name, loader = self._parse_name(target, rewritable, ticker,
    
    114 115
                                                           fetch_subprojects=fetch_subprojects)
    
    115
    -            loader._load_file(name, rewritable, ticker, fetch_subprojects)
    
    116
    +
    
    117
    +            # XXX This will need to be changed to the context's top-level project if this method
    
    118
    +            # is ever used for subprojects
    
    119
    +            top_dir = self.project.directory
    
    120
    +
    
    121
    +            cache_file = YamlCache.get_cache_file(top_dir)
    
    122
    +            with YamlCache.open(self._context, cache_file) as yaml_cache:
    
    123
    +                loader._load_file(name, rewritable, ticker, fetch_subprojects, yaml_cache)
    
    116 124
                 deps.append(Dependency(name, junction=junction))
    
    117 125
                 profile_end(Topics.LOAD_PROJECT, target)
    
    118 126
     
    
    ... ... @@ -201,11 +209,12 @@ class Loader():
    201 209
         #    rewritable (bool): Whether we should load in round trippable mode
    
    202 210
         #    ticker (callable): A callback to report loaded filenames to the frontend
    
    203 211
         #    fetch_subprojects (bool): Whether to fetch subprojects while loading
    
    212
    +    #    yaml_cache (YamlCache): A yaml cache
    
    204 213
         #
    
    205 214
         # Returns:
    
    206 215
         #    (LoadElement): A loaded LoadElement
    
    207 216
         #
    
    208
    -    def _load_file(self, filename, rewritable, ticker, fetch_subprojects):
    
    217
    +    def _load_file(self, filename, rewritable, ticker, fetch_subprojects, yaml_cache=None):
    
    209 218
     
    
    210 219
             # Silently ignore already loaded files
    
    211 220
             if filename in self._elements:
    
    ... ... @@ -218,7 +227,8 @@ class Loader():
    218 227
             # Load the data and process any conditional statements therein
    
    219 228
             fullpath = os.path.join(self._basedir, filename)
    
    220 229
             try:
    
    221
    -            node = _yaml.load(fullpath, shortname=filename, copy_tree=rewritable, project=self.project)
    
    230
    +            node = _yaml.load(fullpath, shortname=filename, copy_tree=rewritable,
    
    231
    +                              project=self.project, yaml_cache=yaml_cache)
    
    222 232
             except LoadError as e:
    
    223 233
                 if e.reason == LoadErrorReason.MISSING_FILE:
    
    224 234
                     # If we can't find the file, try to suggest plausible
    
    ... ... @@ -261,13 +271,13 @@ class Loader():
    261 271
             # Load all dependency files for the new LoadElement
    
    262 272
             for dep in element.deps:
    
    263 273
                 if dep.junction:
    
    264
    -                self._load_file(dep.junction, rewritable, ticker, fetch_subprojects)
    
    274
    +                self._load_file(dep.junction, rewritable, ticker, fetch_subprojects, yaml_cache)
    
    265 275
                     loader = self._get_loader(dep.junction, rewritable=rewritable, ticker=ticker,
    
    266 276
                                               fetch_subprojects=fetch_subprojects)
    
    267 277
                 else:
    
    268 278
                     loader = self
    
    269 279
     
    
    270
    -            dep_element = loader._load_file(dep.name, rewritable, ticker, fetch_subprojects)
    
    280
    +            dep_element = loader._load_file(dep.name, rewritable, ticker, fetch_subprojects, yaml_cache)
    
    271 281
     
    
    272 282
                 if _yaml.node_get(dep_element.node, str, Symbol.KIND) == 'junction':
    
    273 283
                     raise LoadError(LoadErrorReason.INVALID_DATA,
    

  • buildstream/_project.py
    ... ... @@ -19,6 +19,7 @@
    19 19
     #        Tiago Gomes <tiago gomes codethink co uk>
    
    20 20
     
    
    21 21
     import os
    
    22
    +import hashlib
    
    22 23
     from collections import Mapping, OrderedDict
    
    23 24
     from pluginbase import PluginBase
    
    24 25
     from . import utils
    

  • buildstream/_yaml.py
    ... ... @@ -183,20 +183,32 @@ class CompositeTypeError(CompositeError):
    183 183
     #    shortname (str): The filename in shorthand for error reporting (or None)
    
    184 184
     #    copy_tree (bool): Whether to make a copy, preserving the original toplevels
    
    185 185
     #                      for later serialization
    
    186
    +#    yaml_cache (YamlCache): A yaml cache to consult rather than parsing
    
    186 187
     #
    
    187 188
     # Returns (dict): A loaded copy of the YAML file with provenance information
    
    188 189
     #
    
    189 190
     # Raises: LoadError
    
    190 191
     #
    
    191
    -def load(filename, shortname=None, copy_tree=False, *, project=None):
    
    192
    +def load(filename, shortname=None, copy_tree=False, *, project=None, yaml_cache=None):
    
    192 193
         if not shortname:
    
    193 194
             shortname = filename
    
    194 195
     
    
    195 196
         file = ProvenanceFile(filename, shortname, project)
    
    196 197
     
    
    197 198
         try:
    
    199
    +        data = None
    
    198 200
             with open(filename) as f:
    
    199
    -            return load_data(f, file, copy_tree=copy_tree)
    
    201
    +            contents = f.read()
    
    202
    +        if yaml_cache:
    
    203
    +            data, key = yaml_cache.get(project, filename, contents, copy_tree)
    
    204
    +
    
    205
    +        if not data:
    
    206
    +            data = load_data(contents, file, copy_tree=copy_tree)
    
    207
    +
    
    208
    +        if yaml_cache:
    
    209
    +            yaml_cache.put_from_key(project, filename, key, data)
    
    210
    +
    
    211
    +        return data
    
    200 212
         except FileNotFoundError as e:
    
    201 213
             raise LoadError(LoadErrorReason.MISSING_FILE,
    
    202 214
                             "Could not find file at {}".format(filename)) from e
    

  • buildstream/_yamlcache.py
    1
    +#
    
    2
    +#  Copyright 2018 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
    +#        Jonathan Maw <jonathan maw codethink co uk>
    
    19
    +
    
    20
    +import os
    
    21
    +import pickle
    
    22
    +import hashlib
    
    23
    +import io
    
    24
    +
    
    25
    +import sys
    
    26
    +
    
    27
    +from contextlib import contextmanager
    
    28
    +from collections import namedtuple
    
    29
    +
    
    30
    +from ._cachekey import generate_key
    
    31
    +from ._context import Context
    
    32
    +from . import utils, _yaml
    
    33
    +
    
    34
    +
    
    35
    +YAML_CACHE_FILENAME = "yaml_cache.pickle"
    
    36
    +
    
    37
    +
    
    38
    +# YamlCache()
    
    39
    +#
    
    40
    +# A cache that wraps around the loading of yaml in projects.
    
    41
    +#
    
    42
    +# The recommended way to use a YamlCache is:
    
    43
    +#   with YamlCache.open(context) as yamlcache:
    
    44
    +#     # Load all the yaml
    
    45
    +#     ...
    
    46
    +#
    
    47
    +# Args:
    
    48
    +#    context (Context): The invocation Context
    
    49
    +#
    
    50
    +class YamlCache():
    
    51
    +
    
    52
    +    def __init__(self, context):
    
    53
    +        self._project_caches = {}
    
    54
    +        self._context = context
    
    55
    +    ##################
    
    56
    +    # Public Methods #
    
    57
    +    ##################
    
    58
    +
    
    59
    +    # is_cached():
    
    60
    +    #
    
    61
    +    # Checks whether a file is cached.
    
    62
    +    #
    
    63
    +    # Args:
    
    64
    +    #    project (Project): The project this file is in.
    
    65
    +    #    filepath (str): The path to the file, *relative to the project's directory*.
    
    66
    +    #
    
    67
    +    # Returns:
    
    68
    +    #    (bool): Whether the file is cached.
    
    69
    +    def is_cached(self, project, filepath):
    
    70
    +        cache_path = self._get_filepath(project, filepath)
    
    71
    +        project_name = project.name if project else ""
    
    72
    +        try:
    
    73
    +            project_cache = self._project_caches[project_name]
    
    74
    +            if cache_path in project_cache.elements:
    
    75
    +                return True
    
    76
    +        except KeyError:
    
    77
    +            pass
    
    78
    +        return False
    
    79
    +
    
    80
    +    # open():
    
    81
    +    #
    
    82
    +    # Return an instance of the YamlCache which writes to disk when it leaves scope.
    
    83
    +    #
    
    84
    +    # Args:
    
    85
    +    #    context (Context): The context.
    
    86
    +    #    cachefile (str): The path to the cache file.
    
    87
    +    #
    
    88
    +    # Returns:
    
    89
    +    #    (YamlCache): A YamlCache.
    
    90
    +    @staticmethod
    
    91
    +    @contextmanager
    
    92
    +    def open(context, cachefile):
    
    93
    +        # Try to load from disk first
    
    94
    +        cache = None
    
    95
    +        if os.path.exists(cachefile):
    
    96
    +            try:
    
    97
    +                with open(cachefile, "rb") as f:
    
    98
    +                    cache = BstUnpickler(f, context).load()
    
    99
    +            except pickle.UnpicklingError as e:
    
    100
    +                sys.stderr.write("Failed to load YamlCache, {}\n".format(e))
    
    101
    +
    
    102
    +        # Failed to load from disk, create a new one
    
    103
    +        if not cache:
    
    104
    +            cache = YamlCache(context)
    
    105
    +
    
    106
    +        yield cache
    
    107
    +
    
    108
    +        cache._write(cachefile)
    
    109
    +
    
    110
    +    # get_cache_file():
    
    111
    +    #
    
    112
    +    # Retrieves a path to the yaml cache file.
    
    113
    +    #
    
    114
    +    # Returns:
    
    115
    +    #   (str): The path to the cache file
    
    116
    +    @staticmethod
    
    117
    +    def get_cache_file(top_dir):
    
    118
    +        return os.path.join(top_dir, ".bst", YAML_CACHE_FILENAME)
    
    119
    +
    
    120
    +    # get():
    
    121
    +    #
    
    122
    +    # Gets a parsed file from the cache.
    
    123
    +    #
    
    124
    +    # Args:
    
    125
    +    #    project (Project) or None: The project this file is in, if it exists.
    
    126
    +    #    filepath (str): The absolute path to the file.
    
    127
    +    #    contents (str): The contents of the file to be cached
    
    128
    +    #    copy_tree (bool): Whether the data should make a copy when it's being generated
    
    129
    +    #                      (i.e. exactly as when called in yaml)
    
    130
    +    #
    
    131
    +    # Returns:
    
    132
    +    #    (decorated dict): The parsed yaml from the cache, or None if the file isn't in the cache.
    
    133
    +    #    (str):            The key used to look up the parsed yaml in the cache
    
    134
    +    def get(self, project, filepath, contents, copy_tree):
    
    135
    +        key = self._calculate_key(contents, copy_tree)
    
    136
    +        data = self._get(project, filepath, key)
    
    137
    +        return data, key
    
    138
    +
    
    139
    +    # put():
    
    140
    +    #
    
    141
    +    # Puts a parsed file into the cache.
    
    142
    +    #
    
    143
    +    # Args:
    
    144
    +    #    project (Project): The project this file is in.
    
    145
    +    #    filepath (str): The path to the file.
    
    146
    +    #    contents (str): The contents of the file that has been cached
    
    147
    +    #    copy_tree (bool): Whether the data should make a copy when it's being generated
    
    148
    +    #                      (i.e. exactly as when called in yaml)
    
    149
    +    #    value (decorated dict): The data to put into the cache.
    
    150
    +    def put(self, project, filepath, contents, copy_tree, value):
    
    151
    +        key = self._calculate_key(contents, copy_tree)
    
    152
    +        self.put_from_key(project, filepath, key, value)
    
    153
    +
    
    154
    +    # put_from_key():
    
    155
    +    #
    
    156
    +    # Put a parsed file into the cache when given a key.
    
    157
    +    #
    
    158
    +    # Args:
    
    159
    +    #    project (Project): The project this file is in.
    
    160
    +    #    filepath (str): The path to the file.
    
    161
    +    #    key (str): The key to the file within the cache. Typically, this is the
    
    162
    +    #               value of `calculate_key()` with the file's unparsed contents
    
    163
    +    #               and any relevant metadata passed in.
    
    164
    +    #    value (decorated dict): The data to put into the cache.
    
    165
    +    def put_from_key(self, project, filepath, key, value):
    
    166
    +        cache_path = self._get_filepath(project, filepath)
    
    167
    +        project_name = project.name if project else ""
    
    168
    +        try:
    
    169
    +            project_cache = self._project_caches[project_name]
    
    170
    +        except KeyError:
    
    171
    +            project_cache = self._project_caches[project_name] = CachedProject({})
    
    172
    +
    
    173
    +        project_cache.elements[cache_path] = CachedYaml(key, value)
    
    174
    +
    
    175
    +
    
    176
    +    ###################
    
    177
    +    # Private Methods #
    
    178
    +    ###################
    
    179
    +
    
    180
    +    # Writes the yaml cache to the specified path.
    
    181
    +    #
    
    182
    +    # Args:
    
    183
    +    #    path (str): The path to the cache file.
    
    184
    +    def _write(self, path):
    
    185
    +        parent_dir = os.path.dirname(path)
    
    186
    +        os.makedirs(parent_dir, exist_ok=True)
    
    187
    +        with open(path, "wb") as f:
    
    188
    +            BstPickler(f).dump(self)
    
    189
    +
    
    190
    +    # _get_filepath():
    
    191
    +    #
    
    192
    +    # Returns a file path relative to a project if passed, or the original path if
    
    193
    +    # the project is None
    
    194
    +    #
    
    195
    +    # Args:
    
    196
    +    #    project (Project) or None: The project the filepath exists within
    
    197
    +    #    full_path (str): The path that the returned path is based on
    
    198
    +    #
    
    199
    +    # Returns:
    
    200
    +    #    (str): The path to the file, relative to a project if it exists
    
    201
    +    def _get_filepath(self, project, full_path):
    
    202
    +        if project:
    
    203
    +            assert full_path.startswith(project.directory)
    
    204
    +            filepath = os.path.relpath(full_path, project.directory)
    
    205
    +        else:
    
    206
    +            filepath = full_path
    
    207
    +        return full_path
    
    208
    +
    
    209
    +    # _calculate_key():
    
    210
    +    #
    
    211
    +    # Calculates a key for putting into the cache.
    
    212
    +    #
    
    213
    +    # Args:
    
    214
    +    #    (basic object)... : Any number of strictly-ordered basic objects
    
    215
    +    #
    
    216
    +    # Returns:
    
    217
    +    #   (str): A key made out of every arg passed in
    
    218
    +    @staticmethod
    
    219
    +    def _calculate_key(*args):
    
    220
    +        string = pickle.dumps(args)
    
    221
    +        return hashlib.sha1(string).hexdigest()
    
    222
    +
    
    223
    +    # _get():
    
    224
    +    #
    
    225
    +    # Gets a parsed file from the cache when given a key.
    
    226
    +    #
    
    227
    +    # Args:
    
    228
    +    #    project (Project): The project this file is in.
    
    229
    +    #    filepath (str): The path to the file.
    
    230
    +    #    key (str): The key to the file within the cache. Typically, this is the
    
    231
    +    #               value of `calculate_key()` with the file's unparsed contents
    
    232
    +    #               and any relevant metadata passed in.
    
    233
    +    #
    
    234
    +    # Returns:
    
    235
    +    #    (decorated dict): The parsed yaml from the cache, or None if the file isn't in the cache.
    
    236
    +    def _get(self, project, filepath, key):
    
    237
    +        cache_path = self._get_filepath(project, filepath)
    
    238
    +        project_name = project.name if project else ""
    
    239
    +        try:
    
    240
    +            project_cache = self._project_caches[project_name]
    
    241
    +            try:
    
    242
    +                cachedyaml = project_cache.elements[cache_path]
    
    243
    +                if cachedyaml._key == key:
    
    244
    +                    # We've unpickled the YamlCache, but not the specific file
    
    245
    +                    if cachedyaml._contents is None:
    
    246
    +                        cachedyaml._contents = BstUnpickler.loads(cachedyaml._pickled_contents, self._context)
    
    247
    +                    return cachedyaml._contents
    
    248
    +            except KeyError:
    
    249
    +                pass
    
    250
    +        except KeyError:
    
    251
    +            pass
    
    252
    +        return None
    
    253
    +
    
    254
    +
    
    255
    +CachedProject = namedtuple('CachedProject', ['elements'])
    
    256
    +
    
    257
    +
    
    258
    +class CachedYaml():
    
    259
    +    def __init__(self, key, contents):
    
    260
    +        self._key = key
    
    261
    +        self.set_contents(contents)
    
    262
    +
    
    263
    +    # Sets the contents of the CachedYaml.
    
    264
    +    #
    
    265
    +    # Args:
    
    266
    +    #    contents (provenanced dict): The contents to put in the cache.
    
    267
    +    #
    
    268
    +    def set_contents(self, contents):
    
    269
    +        self._contents = contents
    
    270
    +        self._pickled_contents = BstPickler.dumps(contents)
    
    271
    +
    
    272
    +    # Pickling helper method, prevents 'contents' from being serialised
    
    273
    +    def __getstate__(self):
    
    274
    +        data = self.__dict__.copy()
    
    275
    +        data['_contents'] = None
    
    276
    +        return data
    
    277
    +
    
    278
    +
    
    279
    +# In _yaml.load, we have a ProvenanceFile that stores the project the file
    
    280
    +# came from. Projects can't be pickled, but it's always going to be the same
    
    281
    +# project between invocations (unless the entire project is moved but the
    
    282
    +# file stayed in the same place)
    
    283
    +class BstPickler(pickle.Pickler):
    
    284
    +    def persistent_id(self, obj):
    
    285
    +        if isinstance(obj, _yaml.ProvenanceFile):
    
    286
    +            if obj.project:
    
    287
    +                # ProvenanceFile's project object cannot be stored as it is.
    
    288
    +                project_tag = obj.project.name
    
    289
    +                # ProvenanceFile's filename must be stored relative to the
    
    290
    +                # project, as the project dir may move.
    
    291
    +                name = os.path.relpath(obj.name, obj.project.directory)
    
    292
    +            else:
    
    293
    +                project_tag = None
    
    294
    +                name = obj.name
    
    295
    +            return ("ProvenanceFile", name, obj.shortname, project_tag)
    
    296
    +        elif isinstance(obj, Context):
    
    297
    +            return ("Context",)
    
    298
    +        else:
    
    299
    +            return None
    
    300
    +
    
    301
    +    @staticmethod
    
    302
    +    def dumps(obj):
    
    303
    +        stream = io.BytesIO()
    
    304
    +        BstPickler(stream).dump(obj)
    
    305
    +        stream.seek(0)
    
    306
    +        return stream.read()
    
    307
    +
    
    308
    +
    
    309
    +class BstUnpickler(pickle.Unpickler):
    
    310
    +    def __init__(self, file, context):
    
    311
    +        super().__init__(file)
    
    312
    +        self._context = context
    
    313
    +
    
    314
    +    def persistent_load(self, pid):
    
    315
    +        if pid[0] == "ProvenanceFile":
    
    316
    +            _, tagged_name, shortname, project_tag = pid
    
    317
    +
    
    318
    +            if project_tag is not None:
    
    319
    +                for p in self._context.get_projects():
    
    320
    +                    if project_tag == p.name:
    
    321
    +                        project = p
    
    322
    +                        break
    
    323
    +
    
    324
    +                name = os.path.join(project.directory, tagged_name)
    
    325
    +
    
    326
    +                if not project:
    
    327
    +                    projects = [p.name for p in self._context.get_projects()]
    
    328
    +                    raise pickle.UnpicklingError("No project with name {} found in {}"
    
    329
    +                                                 .format(key_id, projects))
    
    330
    +            else:
    
    331
    +                project = None
    
    332
    +                name = tagged_name
    
    333
    +
    
    334
    +            return _yaml.ProvenanceFile(name, shortname, project)
    
    335
    +        elif pid[0] == "Context":
    
    336
    +            return self._context
    
    337
    +        else:
    
    338
    +            raise pickle.UnpicklingError("Unsupported persistent object, {}".format(pid))
    
    339
    +
    
    340
    +    @staticmethod
    
    341
    +    def loads(text, context):
    
    342
    +        stream = io.BytesIO()
    
    343
    +        stream.write(bytes(text))
    
    344
    +        stream.seek(0)
    
    345
    +        return BstUnpickler(stream, context).load()

  • contrib/bst-docker-import
    ... ... @@ -93,10 +93,10 @@ echo "INFO: Checking out $element ..." >&2
    93 93
     $bst_cmd checkout --tar "$element" "$checkout_tar" || die "Failed to checkout $element"
    
    94 94
     echo "INFO: Successfully checked out $element" >&2
    
    95 95
     
    
    96
    -echo "INFO: Importing Docker image ..."
    
    96
    +echo "INFO: Importing Docker image ..." >&2
    
    97 97
     "${docker_import_cmd[@]}" "$checkout_tar" "$docker_image_tag" || die "Failed to import Docker image from tarball"
    
    98
    -echo "INFO: Successfully import Docker image $docker_image_tag"
    
    98
    +echo "INFO: Successfully import Docker image $docker_image_tag" >&2
    
    99 99
     
    
    100
    -echo "INFO: Cleaning up ..."
    
    100
    +echo "INFO: Cleaning up ..." >&2
    
    101 101
     rm "$checkout_tar" || die "Failed to remove $checkout_tar"
    
    102
    -echo "INFO: Clean up finished"
    102
    +echo "INFO: Clean up finished" >&2

  • tests/frontend/yamlcache.py
    1
    +import os
    
    2
    +import pytest
    
    3
    +import hashlib
    
    4
    +import tempfile
    
    5
    +from ruamel import yaml
    
    6
    +
    
    7
    +from tests.testutils import cli, generate_junction, create_element_size, create_repo
    
    8
    +from buildstream import _yaml
    
    9
    +from buildstream._yamlcache import YamlCache
    
    10
    +from buildstream._project import Project
    
    11
    +from buildstream._context import Context
    
    12
    +from contextlib import contextmanager
    
    13
    +
    
    14
    +
    
    15
    +def generate_project(tmpdir, ref_storage, with_junction, name="test"):
    
    16
    +    if with_junction == 'junction':
    
    17
    +        subproject_dir = generate_project(
    
    18
    +            tmpdir, ref_storage,
    
    19
    +            'no-junction', name='test-subproject'
    
    20
    +        )
    
    21
    +
    
    22
    +    project_dir = os.path.join(tmpdir, name)
    
    23
    +    os.makedirs(project_dir)
    
    24
    +    # project.conf
    
    25
    +    project_conf_path = os.path.join(project_dir, 'project.conf')
    
    26
    +    elements_path = 'elements'
    
    27
    +    project_conf = {
    
    28
    +        'name': name,
    
    29
    +        'element-path': elements_path,
    
    30
    +        'ref-storage': ref_storage,
    
    31
    +    }
    
    32
    +    _yaml.dump(project_conf, project_conf_path)
    
    33
    +
    
    34
    +    # elements
    
    35
    +    if with_junction == 'junction':
    
    36
    +        junction_name = 'junction.bst'
    
    37
    +        junction_dir = os.path.join(project_dir, elements_path)
    
    38
    +        junction_path = os.path.join(project_dir, elements_path, junction_name)
    
    39
    +        os.makedirs(junction_dir)
    
    40
    +        generate_junction(tmpdir, subproject_dir, junction_path)
    
    41
    +        element_depends = [{'junction': junction_name, 'filename': 'test.bst'}]
    
    42
    +    else:
    
    43
    +        element_depends = []
    
    44
    +
    
    45
    +    element_name = 'test.bst'
    
    46
    +    create_element_size(element_name, project_dir, elements_path, element_depends, 1)
    
    47
    +
    
    48
    +    return project_dir
    
    49
    +
    
    50
    +
    
    51
    +@contextmanager
    
    52
    +def with_yamlcache(project_dir):
    
    53
    +    context = Context()
    
    54
    +    project = Project(project_dir, context)
    
    55
    +    cache_file = YamlCache.get_cache_file(project_dir)
    
    56
    +    with YamlCache.open(context, cache_file) as yamlcache:
    
    57
    +        yield yamlcache, project
    
    58
    +
    
    59
    +
    
    60
    +def yamlcache_key(yamlcache, in_file, copy_tree=False):
    
    61
    +    with open(in_file) as f:
    
    62
    +        key = yamlcache._calculate_key(f.read(), copy_tree)
    
    63
    +    return key
    
    64
    +
    
    65
    +
    
    66
    +def modified_file(input_file, tmpdir):
    
    67
    +    with open(input_file) as f:
    
    68
    +        data = f.read()
    
    69
    +    assert 'variables' not in data
    
    70
    +    data += '\nvariables: {modified: True}\n'
    
    71
    +    _, temppath = tempfile.mkstemp(dir=tmpdir, text=True)
    
    72
    +    with open(temppath, 'w') as f:
    
    73
    +        f.write(data)
    
    74
    +
    
    75
    +    return temppath
    
    76
    +
    
    77
    +
    
    78
    +@pytest.mark.parametrize('ref_storage', ['inline', 'project.refs'])
    
    79
    +@pytest.mark.parametrize('with_junction', ['no-junction', 'junction'])
    
    80
    +@pytest.mark.parametrize('move_project', ['move', 'no-move'])
    
    81
    +def test_yamlcache_used(cli, tmpdir, ref_storage, with_junction, move_project):
    
    82
    +    # Generate the project
    
    83
    +    project = generate_project(str(tmpdir), ref_storage, with_junction)
    
    84
    +    if with_junction == 'junction':
    
    85
    +        result = cli.run(project=project, args=['fetch', '--track', 'junction.bst'])
    
    86
    +        result.assert_success()
    
    87
    +
    
    88
    +    # bst show to put it in the cache
    
    89
    +    result = cli.run(project=project, args=['show', 'test.bst'])
    
    90
    +    result.assert_success()
    
    91
    +
    
    92
    +    element_path = os.path.join(project, 'elements', 'test.bst')
    
    93
    +    with with_yamlcache(project) as (yc, prj):
    
    94
    +        # Check that it's in the cache
    
    95
    +        assert yc.is_cached(prj, element_path)
    
    96
    +
    
    97
    +        # *Absolutely* horrible cache corruption to check it's being used
    
    98
    +        # Modifying the data from the cache is fraught with danger,
    
    99
    +        # so instead I'll load a modified version of the original file
    
    100
    +        temppath = modified_file(element_path, str(tmpdir))
    
    101
    +        contents = _yaml.load(temppath, copy_tree=False, project=prj)
    
    102
    +        key = yamlcache_key(yc, element_path)
    
    103
    +        yc.put_from_key(prj, element_path, key, contents)
    
    104
    +
    
    105
    +    # Show that a variable has been added
    
    106
    +    result = cli.run(project=project, args=['show', '--format', '%{vars}', 'test.bst'])
    
    107
    +    result.assert_success()
    
    108
    +    data = yaml.safe_load(result.output)
    
    109
    +    assert 'modified' in data
    
    110
    +    assert data['modified'] == 'True'
    
    111
    +
    
    112
    +
    
    113
    +@pytest.mark.parametrize('ref_storage', ['inline', 'project.refs'])
    
    114
    +@pytest.mark.parametrize('with_junction', ['junction', 'no-junction'])
    
    115
    +@pytest.mark.parametrize('move_project', ['move', 'no-move'])
    
    116
    +def test_yamlcache_changed_file(cli, ref_storage, with_junction, move_project):
    
    117
    +    # i.e. a file is cached, the file is changed, loading the file (with cache) returns new data
    
    118
    +    # inline and junction can only be changed by opening a workspace
    
    119
    +    pass

  • tests/yaml/yaml.py
    1 1
     import os
    
    2 2
     import pytest
    
    3
    +import tempfile
    
    3 4
     from collections import Mapping
    
    4 5
     
    
    5 6
     from buildstream import _yaml
    
    6 7
     from buildstream._exceptions import LoadError, LoadErrorReason
    
    8
    +from buildstream._context import Context
    
    9
    +from buildstream._yamlcache import YamlCache
    
    7 10
     
    
    8 11
     DATA_DIR = os.path.join(
    
    9 12
         os.path.dirname(os.path.realpath(__file__)),
    
    ... ... @@ -150,6 +153,21 @@ def test_composite_preserve_originals(datafiles):
    150 153
         assert(_yaml.node_get(orig_extra, str, 'old') == 'new')
    
    151 154
     
    
    152 155
     
    
    156
    +def load_yaml_file(filename, *, cache_path, shortname=None, from_cache='raw'):
    
    157
    +
    
    158
    +    temppath = tempfile.mkstemp(dir=str(cache_path), text=True)
    
    159
    +    context = Context()
    
    160
    +
    
    161
    +    with YamlCache.open(context, str(temppath)) as yc:
    
    162
    +        if from_cache == 'raw':
    
    163
    +            return _yaml.load(filename, shortname)
    
    164
    +        elif from_cache == 'cached':
    
    165
    +            _yaml.load(filename, shortname, yaml_cache=yc)
    
    166
    +            return _yaml.load(filename, shortname, yaml_cache=yc)
    
    167
    +        else:
    
    168
    +            assert False
    
    169
    +
    
    170
    +
    
    153 171
     # Tests for list composition
    
    154 172
     #
    
    155 173
     # Each test composits a filename on top of basics.yaml, and tests
    
    ... ... @@ -165,6 +183,7 @@ def test_composite_preserve_originals(datafiles):
    165 183
     #    prov_col: The expected provenance column of "mood"
    
    166 184
     #
    
    167 185
     @pytest.mark.datafiles(os.path.join(DATA_DIR))
    
    186
    +@pytest.mark.parametrize('caching', [('raw'), ('cached')])
    
    168 187
     @pytest.mark.parametrize("filename,index,length,mood,prov_file,prov_line,prov_col", [
    
    169 188
     
    
    170 189
         # Test results of compositing with the (<) prepend directive
    
    ... ... @@ -195,14 +214,15 @@ def test_composite_preserve_originals(datafiles):
    195 214
         ('implicitoverwrite.yaml', 0, 2, 'overwrite1', 'implicitoverwrite.yaml', 4, 8),
    
    196 215
         ('implicitoverwrite.yaml', 1, 2, 'overwrite2', 'implicitoverwrite.yaml', 6, 8),
    
    197 216
     ])
    
    198
    -def test_list_composition(datafiles, filename,
    
    217
    +def test_list_composition(datafiles, filename, tmpdir,
    
    199 218
                               index, length, mood,
    
    200
    -                          prov_file, prov_line, prov_col):
    
    201
    -    base = os.path.join(datafiles.dirname, datafiles.basename, 'basics.yaml')
    
    202
    -    overlay = os.path.join(datafiles.dirname, datafiles.basename, filename)
    
    219
    +                          prov_file, prov_line, prov_col, caching):
    
    220
    +    base_file = os.path.join(datafiles.dirname, datafiles.basename, 'basics.yaml')
    
    221
    +    overlay_file = os.path.join(datafiles.dirname, datafiles.basename, filename)
    
    222
    +
    
    223
    +    base = load_yaml_file(base_file, cache_path=tmpdir, shortname='basics.yaml', from_cache=caching)
    
    224
    +    overlay = load_yaml_file(overlay_file, cache_path=tmpdir, shortname=filename, from_cache=caching)
    
    203 225
     
    
    204
    -    base = _yaml.load(base, shortname='basics.yaml')
    
    205
    -    overlay = _yaml.load(overlay, shortname=filename)
    
    206 226
         _yaml.composite_dict(base, overlay)
    
    207 227
     
    
    208 228
         children = _yaml.node_get(base, list, 'children')
    
    ... ... @@ -254,6 +274,7 @@ def test_list_deletion(datafiles):
    254 274
     #    prov_col: The expected provenance column of "mood"
    
    255 275
     #
    
    256 276
     @pytest.mark.datafiles(os.path.join(DATA_DIR))
    
    277
    +@pytest.mark.parametrize('caching', [('raw'), ('cached')])
    
    257 278
     @pytest.mark.parametrize("filename1,filename2,index,length,mood,prov_file,prov_line,prov_col", [
    
    258 279
     
    
    259 280
         # Test results of compositing literal list with (>) and then (<)
    
    ... ... @@ -310,9 +331,9 @@ def test_list_deletion(datafiles):
    310 331
         ('listoverwrite.yaml', 'listprepend.yaml', 2, 4, 'overwrite1', 'listoverwrite.yaml', 5, 10),
    
    311 332
         ('listoverwrite.yaml', 'listprepend.yaml', 3, 4, 'overwrite2', 'listoverwrite.yaml', 7, 10),
    
    312 333
     ])
    
    313
    -def test_list_composition_twice(datafiles, filename1, filename2,
    
    334
    +def test_list_composition_twice(datafiles, tmpdir, filename1, filename2,
    
    314 335
                                     index, length, mood,
    
    315
    -                                prov_file, prov_line, prov_col):
    
    336
    +                                prov_file, prov_line, prov_col, caching):
    
    316 337
         file_base = os.path.join(datafiles.dirname, datafiles.basename, 'basics.yaml')
    
    317 338
         file1 = os.path.join(datafiles.dirname, datafiles.basename, filename1)
    
    318 339
         file2 = os.path.join(datafiles.dirname, datafiles.basename, filename2)
    
    ... ... @@ -320,9 +341,9 @@ def test_list_composition_twice(datafiles, filename1, filename2,
    320 341
         #####################
    
    321 342
         # Round 1 - Fight !
    
    322 343
         #####################
    
    323
    -    base = _yaml.load(file_base, shortname='basics.yaml')
    
    324
    -    overlay1 = _yaml.load(file1, shortname=filename1)
    
    325
    -    overlay2 = _yaml.load(file2, shortname=filename2)
    
    344
    +    base = load_yaml_file(file_base, cache_path=tmpdir, shortname='basics.yaml', from_cache=caching)
    
    345
    +    overlay1 = load_yaml_file(file1, cache_path=tmpdir, shortname=filename1, from_cache=caching)
    
    346
    +    overlay2 = load_yaml_file(file2, cache_path=tmpdir, shortname=filename2, from_cache=caching)
    
    326 347
     
    
    327 348
         _yaml.composite_dict(base, overlay1)
    
    328 349
         _yaml.composite_dict(base, overlay2)
    
    ... ... @@ -337,9 +358,9 @@ def test_list_composition_twice(datafiles, filename1, filename2,
    337 358
         #####################
    
    338 359
         # Round 2 - Fight !
    
    339 360
         #####################
    
    340
    -    base = _yaml.load(file_base, shortname='basics.yaml')
    
    341
    -    overlay1 = _yaml.load(file1, shortname=filename1)
    
    342
    -    overlay2 = _yaml.load(file2, shortname=filename2)
    
    361
    +    base = load_yaml_file(file_base, cache_path=tmpdir, shortname='basics.yaml', from_cache=caching)
    
    362
    +    overlay1 = load_yaml_file(file1, cache_path=tmpdir, shortname=filename1, from_cache=caching)
    
    363
    +    overlay2 = load_yaml_file(file2, cache_path=tmpdir, shortname=filename2, from_cache=caching)
    
    343 364
     
    
    344 365
         _yaml.composite_dict(overlay1, overlay2)
    
    345 366
         _yaml.composite_dict(base, overlay1)
    



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