Repository: swcarpentry/python-novice-inflammation

Issue:
Solve part of #287
Explain word is a collection of characters in episode about loops and corrected a typo


Discussion:
Thank you, Susan! I think these are two nice changes to the episode.

Could you please split your PR into two: (1) with the text addition, and (2) with a typo fix?
(when fixing a typo, could you please also add a comma after "For example"?)

If you're comfortable with the command line, you can look at our [wiki](https://github.com/swcarpentry/python-novice-inflammation/wiki) for instructions on how to [Fix a PR created from the gh-pages branch](https://github.com/swcarpentry/python-novice-inflammation/wiki/Fixing-a-PR-created-from-the-gh-pages-branch) and [Split Pull Requests](https://github.com/swcarpentry/python-novice-inflammation/wiki/Splitting-(Breaking-down)-Pull-Requests) -- let me know if you have any questions or need help with anything.
Replaced by #643 and #644

Resolving patch:
diff --git a/bin/lesson_check.py b/bin/lesson_check.py
--- a/bin/lesson_check.py
+++ b/bin/lesson_check.py
@@ -6,9 +6,10 @@
 import os
 import glob
 import re
+import sys
 from argparse import ArgumentParser
 
-# This uses the `__all__` list in `util.py` to determine what objects to import 
+# This uses the `__all__` list in `util.py` to determine what objects to import
 # see https://docs.python.org/3/tutorial/modules.html#importing-from-a-package
 from util import *
 from reporter import Reporter
@@ -118,21 +119,29 @@
 # Please keep this in sync with .editorconfig!
 MAX_LINE_LEN = 100
 
+# Contents of _config.yml
+CONFIG = {}
 
 def main():
     """Main driver."""
 
     args = parse_args()
     args.reporter = Reporter()
-    life_cycle = check_config(args.reporter, args.source_dir)
+
+    global CONFIG
+    config_file = os.path.join(args.source_dir, '_config.yml')
+    CONFIG = load_yaml(config_file)
+    CONFIG["config_file"] = config_file
+
+    life_cycle = CONFIG.get('life_cycle', None)
     # pre-alpha lessons should report without error
     if life_cycle == "pre-alpha":
         args.permissive = True
+
+    check_config(args.reporter)
     check_source_rmd(args.reporter, args.source_dir, args.parser)
 
-    args.references = {}
-    if not using_remote_theme(args.source_dir):
-        args.references = read_references(args.reporter, args.reference_path)
+    args.references = read_references(args.reporter, args.reference_path)
 
     docs = read_all_markdown(args.source_dir, args.parser)
     check_fileset(args.source_dir, args.reporter, list(docs.keys()))
@@ -142,8 +151,16 @@ def main():
         checker.check()
 
     args.reporter.report()
-    if args.reporter.messages and not args.permissive:
-        exit(1)
+    if args.reporter.messages:
+        if args.permissive:
+            print("Problems detected but ignored (permissive mode).")
+        else:
+            print("Problems detected.")
+            sys.exit(1)
+    else:
+        print("No problems found.")
+
+    return
 
 
 def parse_args():
@@ -180,40 +197,35 @@ def parse_args():
 
     args, extras = parser.parse_known_args()
     require(args.parser is not None,
-            'Path to Markdown parser not provided')
+            'Path to Markdown parser not provided',
+            True)
     require(not extras,
             'Unexpected trailing command-line arguments "{0}"'.format(extras))
 
     return args
 
-def using_remote_theme(source_dir):
-    config_file = os.path.join(source_dir, '_config.yml')
-    config = load_yaml(config_file)
-    return 'remote_theme' in config
-
-def check_config(reporter, source_dir):
+def check_config(reporter):
     """Check configuration file."""
 
-    config_file = os.path.join(source_dir, '_config.yml')
-    config = load_yaml(config_file)
-    reporter.check_field(config_file, 'configuration',
-                         config, 'kind', 'lesson')
-    reporter.check_field(config_file, 'configuration',
-                         config, 'carpentry', ('swc', 'dc', 'lc', 'cp', 'incubator'))
-    reporter.check_field(config_file, 'configuration', config, 'title')
-    reporter.check_field(config_file, 'configuration', config, 'email')
+    reporter.check_field(CONFIG["config_file"], 'configuration',
+                         CONFIG, 'kind', 'lesson')
+    reporter.check_field(CONFIG["config_file"], 'configuration',
+                         CONFIG, 'carpentry', ('swc', 'dc', 'lc', 'cp', 'incubator'))
+    reporter.check_field(CONFIG["config_file"], 'configuration', CONFIG, 'title')
+    reporter.check_field(CONFIG["config_file"], 'configuration', CONFIG, 'email')
 
     for defaults in [
             {'values': {'root': '.', 'layout': 'page'}},
             {'values': {'root': '..', 'layout': 'episode'}, 'scope': {'type': 'episodes', 'path': ''}},
             {'values': {'root': '..', 'layout': 'page'}, 'scope': {'type': 'extras', 'path': ''}}
             ]:
-        reporter.check(defaults in config.get('defaults', []),
-                   'configuration',
-                   '"root" not set to "." in configuration')
-    if 'life_cycle' not in config:
-        config['life_cycle'] = None
-    return config['life_cycle']
+        error_text = 'incorrect settings for: root "{0}" layout "{1}"'
+        root = defaults["values"]["root"]
+        layout = defaults["values"]["layout"]
+        error_message = error_text.format(root, layout)
+
+        defaults_test = defaults in CONFIG.get('defaults', [])
+        reporter.check(defaults_test, 'configuration', error_message)
 
 def check_source_rmd(reporter, source_dir, parser):
     """Check that Rmd episode files include `source: Rmd`"""
@@ -234,6 +246,9 @@ def read_references(reporter, ref_path):
     {symbolic_name : URL}
     """
 
+    if 'remote_theme' in CONFIG:
+        return {}
+
     if not ref_path:
         raise Warning("No filename has been provided.")
 
@@ -533,8 +548,7 @@ def check(self):
         """Run extra tests."""
 
         super().check()
-        if not using_remote_theme(args.source_dir):
-            self.check_reference_inclusion()
+        self.check_reference_inclusion()
 
     def check_metadata(self):
         super().check_metadata()
@@ -564,6 +578,9 @@ def check_metadata_fields(self, expected):
     def check_reference_inclusion(self):
         """Check that links file has been included."""
 
+        if 'remote_theme' in CONFIG:
+            return
+
         if not self.args.reference_path:
             return
 
@@ -601,7 +618,8 @@ def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
     (re.compile(r'README\.md'), CheckNonJekyll),
     (re.compile(r'index\.md'), CheckIndex),
     (re.compile(r'reference\.md'), CheckReference),
-    (re.compile(os.path.join('_episodes', '*\.md')), CheckEpisode),
+    # '.' below is what's passed on the command line via '-s' flag
+    (re.compile(os.path.join('.','_episodes', '[^/]*\.md')), CheckEpisode),
     (re.compile(r'.*\.md'), CheckGeneric)
 ]
 
diff --git a/bin/lesson_initialize.py b/bin/lesson_initialize.py
--- a/bin/lesson_initialize.py
+++ b/bin/lesson_initialize.py
@@ -6,7 +6,6 @@
 import shutil
 
 BOILERPLATE = (
-    '.travis.yml',
     'AUTHORS',
     'CITATION',
     'CONTRIBUTING.md',
diff --git a/bin/util.py b/bin/util.py
--- a/bin/util.py
+++ b/bin/util.py
@@ -65,9 +65,8 @@ def split_metadata(path, text):
         try:
             metadata_yaml = yaml.load(metadata_raw, Loader=yaml.SafeLoader)
         except yaml.YAMLError as e:
-            print('Unable to parse YAML header in {0}:\n{1}'.format(
-                path, e), file=sys.stderr)
-            sys.exit(1)
+            message = 'Unable to parse YAML header in {0}:\n{1}'
+            print(message.format(path, e), file=sys.stderr)
 
     return metadata_raw, metadata_yaml, text
 
@@ -81,11 +80,14 @@ def load_yaml(filename):
     try:
         with open(filename, 'r', encoding='utf-8') as reader:
             return yaml.load(reader, Loader=yaml.SafeLoader)
-    except (yaml.YAMLError, IOError) as e:
-        print('Unable to load YAML file {0}:\n{1}'.format(
-            filename, e), file=sys.stderr)
-        sys.exit(1)
+    except yaml.YAMLError as e:
+        message = 'ERROR: Unable to load YAML file {0}:\n{1}'
+        print(message.format(filename, e), file=sys.stderr)
+    except (FileNotFoundError, IOError):
+        message = 'ERROR: File {} not found'
+        print(message.format(filename), file=sys.stderr)
 
+    return {}
 
 def check_unwanted_files(dir_path, reporter):
     """
@@ -99,9 +101,11 @@ def check_unwanted_files(dir_path, reporter):
                        "Unwanted file found")
 
 
-def require(condition, message):
+def require(condition, message, fatal=False):
     """Fail if condition not met."""
 
     if not condition:
         print(message, file=sys.stderr)
-        sys.exit(1)
+
+        if fatal:
+            sys.exit(1)


Test patch: