Repository: ARMmbed/yotta (python)

Issue:
Discourage accidental use of mbed-official keyword
There have been several cases where target descriptions have been published with descriptions and keywords indicating that they are officially supported, when in fact they aren't.

yotta should warn when these keywords are used, to make sure that their use is intentional.

Pull request description:
warn and insist on confirmation when using -official keywords; add test for this

cc @0xc0170 

Should resolve #655 

<img width="522" alt="screen shot 2016-01-12 at 15 28 59" src="https://cloud.githubusercontent.com/assets/3456211/12267689/3aefe400-b941-11e5-8d67-da9efe392ee0.png">

Patch:
diff --git a/yotta/lib/pack.py b/yotta/lib/pack.py
index ade5aae..f7ef9d0 100644
--- a/yotta/lib/pack.py
+++ b/yotta/lib/pack.py
@@ -257,6 +257,12 @@ class Pack(object):
         else:
             return None
 
+    def getKeywords(self):
+        if self.description:
+            return self.description.get('keywords', [])
+        else:
+            return []
+
     def _parseIgnoreFile(self, f):
         r = []
         for l in f:
diff --git a/yotta/options/__init__.py b/yotta/options/__init__.py
index 1482b43..aa66e80 100644
--- a/yotta/options/__init__.py
+++ b/yotta/options/__init__.py
@@ -11,6 +11,7 @@ from . import noninteractive
 from . import registry
 from . import target
 from . import config
+from . import force
 
 # this modifies argparse when it's imported:
 from . import parser
diff --git a/yotta/options/force.py b/yotta/options/force.py
new file mode 100644
index 0000000..33c8e6e
--- /dev/null
+++ b/yotta/options/force.py
@@ -0,0 +1,12 @@
+# Copyright 2014-2015 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0
+# See LICENSE file for details.
+
+def addTo(parser):
+    parser.add_argument('-f', '--force', action='store_true', dest="force",
+        help='Force the operation to (try to) continue even in situations which '+
+             'would be an error.'
+    )
+
+
diff --git a/yotta/publish.py b/yotta/publish.py
index 28d999b..721d932 100644
--- a/yotta/publish.py
+++ b/yotta/publish.py
@@ -8,11 +8,50 @@ import logging
 
 # validate, , validate things, internal
 from .lib import validate
+# options, , shared options, internal
+import yotta.options as options
 
 def addOptions(parser):
-    # no options
+    options.force.addTo(parser)
+
+# python 2 + 3 compatibility
+try:
+    global input
+    input = raw_input
+except NameError:
     pass
 
+def prePublishCheck(p, force=False, interactive=True):
+    need_ok = False
+    if p.description.get('bin', None) is not None:
+        logging.warning(
+            'This is an executable application, not a re-usable library module. Other modules will not be able to depend on it!'
+        )
+        need_ok = True
+
+    official_keywords = [x for x in p.getKeywords() if x.endswith('-official')]
+    if len(official_keywords):
+        need_ok = True
+        for k in official_keywords:
+            prefix = k[:-len('-official')]
+            logging.warning(
+                ('You\'re publishing with the %s tag. Is this really an '+
+                'officially supported %s module? If not, please remove the %s '+
+                'tag from your %s file. If you are unsure, please ask on the '+
+                'issue tracker.') % (
+                    k, prefix, k, p.description_filename
+                )
+            )
+
+    if need_ok and not interactive:
+        logging.error('--noninteractive prevents user confirmation. Please re-run with --force')
+        return 1
+
+    if need_ok and not force:
+        input("If you still want to publish, press [enter] to continue.")
+
+    return 0
+
 def execCommand(args, following_args):
     p = validate.currentDirectoryModuleOrTarget()
     if not p:
@@ -22,17 +61,9 @@ def execCommand(args, following_args):
         logging.error('The working directory is not clean. Commit before publishing!')
         return 1
 
-    if p.description.get('bin', None) is not None:
-        logging.warning(
-            'This is an executable application, not a re-usable library module. Other modules will not be able to depend on it!'
-        )
-        # python 2 + 3 compatibility
-        try:
-            global input
-            input = raw_input
-        except NameError:
-            pass
-        raw_input("If you still want to publish it, press [enter] to continue.")
+    errcode = prePublishCheck(p, args.force, args.interactive)
+    if errcode and not args.force:
+        return errcode
 
     error = p.publish(args.registry)
     if error:


Test patch:
diff --git a/yotta/test/cli/test_publish.py b/yotta/test/cli/test_publish.py
index de93d77..11b713e 100644
--- a/yotta/test/cli/test_publish.py
+++ b/yotta/test/cli/test_publish.py
@@ -13,7 +13,7 @@ import tempfile
 # internal modules:
 from yotta.lib.fsutils import rmRf
 from . import cli
-
+from . import util
 
 Test_Target = "x86-osx-native,*"
 
@@ -54,6 +54,26 @@ Public_Module_JSON = '''{
 }'''
 
 
+Test_Publish = {
+'module.json':'''{
+  "name": "test-publish",
+  "version": "0.0.0",
+  "description": "Test yotta publish",
+  "author": "James Crosby <james.crosby@arm.com>",
+  "license": "Apache-2.0",
+  "keywords": ["mbed-official"],
+  "dependencies":{
+  }
+}''',
+'readme.md':'''##This is a test module used in yotta's test suite.''',
+'source/foo.c':'''#include "stdio.h"
+int foo(){
+    printf("foo!\\n");
+    return 7;
+}'''
+}
+
+
 class TestCLIPublish(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
@@ -89,6 +109,15 @@ class TestCLIPublish(unittest.TestCase):
             else:
                 del os.environ['YOTTA_USER_SETTINGS_DIR']
 
+    def test_warnOfficialKeywords(self):
+        path = util.writeTestFiles(Test_Publish, True)
+
+        stdout, stderr, statuscode = cli.run(['-t', 'x86-linux-native', '--noninteractive', 'publish'], cwd=path)
+        self.assertNotEqual(statuscode, 0)
+        self.assertIn('Is this really an officially supported mbed module', stdout + stderr)
+
+        util.rmRf(path)
+
 if __name__ == '__main__':
     unittest.main()