Repository: conda/conda-build

Issue:
Order of items in `requirements` and `about` are not preserved by skeleton on python 2
The order of the entries in the `requirements` and `about` sections is correct in python 3 but not python 2.7 (both with conda build 3.0.8). The order is set at https://github.com/conda/conda-build/blob/master/conda_build/skeletons/pypi.py#L62

I might have time to look into this in a day or two.  


Discussion:

Resolving patch:
diff --git a/conda_build/cli/main_render.py b/conda_build/cli/main_render.py
--- a/conda_build/cli/main_render.py
+++ b/conda_build/cli/main_render.py
@@ -10,7 +10,8 @@
 import sys
 import os
 
-from conda_build.conda_interface import ArgumentParser, add_parser_channels, cc_conda_build, url_path
+from conda_build.conda_interface import (ArgumentParser, add_parser_channels, cc_conda_build,
+                                         url_path)
 
 from conda_build import __version__, api
 
diff --git a/conda_build/skeletons/pypi.py b/conda_build/skeletons/pypi.py
--- a/conda_build/skeletons/pypi.py
+++ b/conda_build/skeletons/pypi.py
@@ -80,23 +80,26 @@
 
 """
 
+# To preserve order of the output in each of the sections the data type
+# needs to be ordered.
+# The top-level ordering is irrelevant because the write order of 'package',
+# etc. is determined by EXPECTED_SECTION_ORDER.
 PYPI_META_STATIC = {
-    'package': {
-        'name': '{{ name|lower }}',
-        'version': '{{ version }}',
-    },
-    'source': {
-        'fn': '{{ name }}-{{ version }}.tar.gz',
-        'url': 'https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz',  # NOQA
-        '{{ hash_type }}': '{{ hash_value }}',
-
-    },
-    'build': {
-        'number': 0,
-    },
-    'extra': {
-        'recipe-maintainers': ''
-    },
+    'package': ruamel_yaml.comments.CommentedMap([
+        ('name', '{{ name|lower }}'),
+        ('version', '{{ version }}'),
+    ]),
+    'source': ruamel_yaml.comments.CommentedMap([
+        ('fn', '{{ name }}-{{ version }}.tar.gz'),
+        ('url', 'https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz'),  # NOQA
+        ('{{ hash_type }}', '{{ hash_value }}'),
+    ]),
+    'build': ruamel_yaml.comments.CommentedMap([
+        ('number', 0),
+    ]),
+    'extra': ruamel_yaml.comments.CommentedMap([
+        ('recipe-maintainers', '')
+    ]),
 }
 
 # Note the {} formatting bits here
@@ -285,7 +288,7 @@ def skeletonize(packages, output_dir=".", version=None, recursive=False,
                 try:
                     ordered_recipe[key] = PYPI_META_STATIC[key]
                 except KeyError:
-                    ordered_recipe[key] = {}
+                    ordered_recipe[key] = ruamel_yaml.comments.CommentedMap()
 
             if d['entry_points']:
                 ordered_recipe['build']['entry_points'] = d['entry_points']
@@ -299,7 +302,7 @@ def skeletonize(packages, output_dir=".", version=None, recursive=False,
                                                       '--record=record.txt')
 
             # Always require python as a dependency
-            ordered_recipe['requirements'] = {}
+            ordered_recipe['requirements'] = ruamel_yaml.comments.CommentedMap()
             ordered_recipe['requirements']['build'] = ['python'] + ensure_list(d['build_depends'])
             ordered_recipe['requirements']['run'] = ['python'] + ensure_list(d['run_depends'])
 
@@ -312,7 +315,7 @@ def skeletonize(packages, output_dir=".", version=None, recursive=False,
             if d['tests_require']:
                 ordered_recipe['test']['requires'] = d['tests_require']
 
-            ordered_recipe['about'] = {}
+            ordered_recipe['about'] = ruamel_yaml.comments.CommentedMap()
 
             for key in ABOUT_ORDER:
                 try:


Test patch:
diff --git a/tests/test_api_skeleton.py b/tests/test_api_skeleton.py
--- a/tests/test_api_skeleton.py
+++ b/tests/test_api_skeleton.py
@@ -4,6 +4,16 @@
 from pkg_resources import parse_version
 import pytest
 
+try:
+    import ruamel_yaml
+except ImportError:
+    try:
+        import ruamel.yaml as ruamel_yaml
+    except ImportError:
+        raise ImportError("No ruamel_yaml library available.\n"
+                          "To proceed, conda install ruamel_yaml")
+
+
 from conda_build import api
 from conda_build.exceptions import DependencyNeedsBuildingError
 
@@ -152,3 +162,35 @@ def test_setuptools_test_requirements(testing_workdir):
     api.skeletonize(packages='hdf5storage', repo='pypi')
     m = api.render('hdf5storage')[0][0]
     assert m.meta['test']['requires'] == ['nose >=1.0']
+
+
+@pytest.mark.serial
+def test_pypi_section_order_preserved(testing_workdir):
+    """
+    Test whether sections have been written in the correct order.
+    """
+    from conda_build.render import FIELDS
+    from conda_build.skeletons.pypi import (ABOUT_ORDER,
+                                            REQUIREMENTS_ORDER,
+                                            PYPI_META_STATIC)
+
+    api.skeletonize(packages='sympy', repo='pypi')
+    # Since we want to check the order of items in the recipe (not whether
+    # the metadata values themselves are sensible), read the file as (ordered)
+    # yaml, and check the order.
+    with open('sympy/meta.yaml', 'r') as file:
+        lines = [l for l in file.readlines() if not l.startswith("{%")]
+
+    # The loader below preserves the order of entries...
+    recipe = ruamel_yaml.load('\n'.join(lines),
+                              Loader=ruamel_yaml.RoundTripLoader)
+
+    major_sections = list(recipe.keys())
+    # Blank fields are omitted when skeletonizing, so prune any missing ones
+    # before comparing.
+    pruned_fields = [f for f in FIELDS if f in major_sections]
+    assert major_sections == pruned_fields
+    assert list(recipe['about']) == ABOUT_ORDER
+    assert list(recipe['requirements']) == REQUIREMENTS_ORDER
+    for k, v in PYPI_META_STATIC.items():
+        assert list(v.keys()) == list(recipe[k])