diff --git a/autocmake/http.py b/autocmake/http.py new file mode 100644 index 0000000..4ef6b1d --- /dev/null +++ b/autocmake/http.py @@ -0,0 +1,28 @@ +def fetch_url(src, dst): + """ + Fetch file from URL src and save it to dst. + """ + # we do not use the nicer sys.version_info.major + # for compatibility with Python < 2.7 + if sys.version_info[0] > 2: + import urllib.request + + class URLopener(urllib.request.FancyURLopener): + def http_error_default(self, url, fp, errcode, errmsg, headers): + sys.stderr.write("ERROR: could not fetch {0}\n".format(url)) + sys.exit(-1) + else: + import urllib + + class URLopener(urllib.FancyURLopener): + def http_error_default(self, url, fp, errcode, errmsg, headers): + sys.stderr.write("ERROR: could not fetch {0}\n".format(url)) + sys.exit(-1) + + dirname = os.path.dirname(dst) + if dirname != '': + if not os.path.isdir(dirname): + os.makedirs(dirname) + + opener = URLopener() + opener.retrieve(src, dst) diff --git a/autocmake/interpolate.py b/autocmake/interpolate.py new file mode 100644 index 0000000..0d371ab --- /dev/null +++ b/autocmake/interpolate.py @@ -0,0 +1,47 @@ +def replace(s, d): + from re import findall + if isinstance(s, str): + for var in findall(r"%\(([A-Za-z0-9_]*)\)", s): + s = s.replace("%({})".format(var), str(d[var])) + return s + + +def test_replace(): + assert replace('hey %(foo) ho %(bar)', + {'foo': 'hey', 'bar': 'ho'}) == 'hey hey ho ho' + + +def interpolate(d, d_map): + from collections import Mapping, Iterable + from copy import copy + for k, v in d.items(): + if isinstance(v, Mapping): + d[k] = interpolate(d[k], d_map) + elif isinstance(v, Iterable) and not isinstance(v, str): + l = [] + for x in v: + if isinstance(x, Mapping): + l.append(interpolate(x, d_map)) + else: + l.append(replace(x, d_map)) + d[k] = copy(l) + else: + d[k] = replace(d[k], d_map) + return d + + +def test_interpolate(): + d = {'foo': 'hey', + 'bar': 'ho', + 'one': 'hey %(foo) ho %(bar)', + 'two': {'one': 'hey %(foo) ho %(bar)', + 'two': 'raboof'}} + d_interpolated = {'foo': 'hey', + 'bar': 'ho', + 'one': 'hey hey ho ho', + 'two': {'one': 'hey hey ho ho', + 'two': 'raboof'}} + assert interpolate(d, d) == d_interpolated + d2 = {'modules': [{'fc': [{'source': '%(url_root)fc_optional.cmake'}]}], 'url_root': 'downloaded/downloaded_'} + d2_interpolated = {'modules': [{'fc': [{'source': 'downloaded/downloaded_fc_optional.cmake'}]}], 'url_root': 'downloaded/downloaded_'} + assert interpolate(d2, d2) == d2_interpolated diff --git a/autocmake/parse_rst.py b/autocmake/parse_rst.py new file mode 100644 index 0000000..70a2151 --- /dev/null +++ b/autocmake/parse_rst.py @@ -0,0 +1,133 @@ +def parse_cmake_module(s_in, override={}): + import sys + from collections import Mapping, Iterable, defaultdict + from autocmake.parse_yaml import parse_yaml + + # we do not use the nicer sys.version_info.major + # for compatibility with Python < 2.7 + if sys.version_info[0] > 2: + from io import StringIO + else: + from StringIO import StringIO + + parsed_config = defaultdict(lambda: None) + + if 'autocmake.yml configuration::' not in s_in: + return parsed_config + + s_out = [] + is_rst_line = False + for line in s_in.split('\n'): + if is_rst_line: + if len(line) > 0: + if line[0] != '#': + is_rst_line = False + else: + is_rst_line = False + if is_rst_line: + s_out.append(line[2:]) + if '#.rst:' in line: + is_rst_line = True + + autocmake_entry = '\n'.join(s_out).split('autocmake.yml configuration::')[1] + autocmake_entry = autocmake_entry.replace('\n ', '\n') + + buf = StringIO(autocmake_entry) + config = parse_yaml(buf, override) + + for k, v in config.items(): + if isinstance(v, Iterable) and not isinstance(v, str): + parsed_config[k] = [x for x in config[k]] + else: + parsed_config[k] = [config[k]] + + return parsed_config + + +def test_parse_cmake_module(): + + s = r'''#.rst: +# +# Foo ... +# +# autocmake.yml configuration:: +# +# docopt: +# - "--cxx= C++ compiler [default: g++]." +# - "--extra-cxx-flags= Extra C++ compiler flags [default: '']." +# export: "'CXX={0}'.format(arguments['--cxx'])" +# define: "'-DEXTRA_CXXFLAGS=\"{0}\"'.format(arguments['--extra-cxx-flags'])" + +enable_language(CXX) + +if(NOT DEFINED CMAKE_C_COMPILER_ID) + message(FATAL_ERROR "CMAKE_C_COMPILER_ID variable is not defined!") +endif()''' + + parsed_config = parse_cmake_module(s) + assert parsed_config['docopt'] == ["--cxx= C++ compiler [default: g++].", "--extra-cxx-flags= Extra C++ compiler flags [default: '']."] + + +def test_parse_cmake_module_no_key(): + + s = '''#.rst: +# +# Foo ... +# +# Bar ... + +enable_language(CXX) + +if(NOT DEFINED CMAKE_C_COMPILER_ID) + message(FATAL_ERROR "CMAKE_C_COMPILER_ID variable is not defined!") +endif()''' + + parsed_config = parse_cmake_module(s) + assert parsed_config['docopt'] is None + + +def test_parse_cmake_module_interpolate(): + + s = r'''#.rst: +# +# Foo ... +# +# autocmake.yml configuration:: +# +# major: 1 +# minor: 2 +# patch: 3 +# a: v%(major) +# b: v%(minor) +# c: v%(patch) + +enable_language(CXX)''' + + parsed_config = parse_cmake_module(s) + assert parsed_config['a'] == ['v1'] + assert parsed_config['b'] == ['v2'] + assert parsed_config['c'] == ['v3'] + + +def test_parse_cmake_module_override(): + + s = r'''#.rst: +# +# Foo ... +# +# autocmake.yml configuration:: +# +# major: 1 +# minor: 2 +# patch: 3 +# a: v%(major) +# b: v%(minor) +# c: v%(patch) + +enable_language(CXX)''' + + d = {'minor': 4} + parsed_config = parse_cmake_module(s, d) + assert parsed_config['a'] == ['v1'] + assert parsed_config['b'] == ['v4'] + assert parsed_config['c'] == ['v3'] diff --git a/autocmake/parse_yaml.py b/autocmake/parse_yaml.py new file mode 100644 index 0000000..1824b38 --- /dev/null +++ b/autocmake/parse_yaml.py @@ -0,0 +1,16 @@ +def parse_yaml(stream, override={}): + import yaml + from autocmake.interpolate import interpolate + + try: + config = yaml.load(stream, yaml.SafeLoader) + except yaml.YAMLError as exc: + print(exc) + sys.exit(-1) + + for k in config: + if k in override: + config[k] = override[k] + + config = interpolate(config, config) + return config diff --git a/update.py b/update.py index ff31978..56a5238 100644 --- a/update.py +++ b/update.py @@ -8,119 +8,12 @@ import collections __version__ = 'X.Y.Z' -# we do not use the nicer sys.version_info.major -# for compatibility with Python < 2.7 -if sys.version_info[0] > 2: - from io import StringIO - import urllib.request - - class URLopener(urllib.request.FancyURLopener): - def http_error_default(self, url, fp, errcode, errmsg, headers): - sys.stderr.write("ERROR: could not fetch {0}\n".format(url)) - sys.exit(-1) -else: - from StringIO import StringIO - import urllib - - class URLopener(urllib.FancyURLopener): - def http_error_default(self, url, fp, errcode, errmsg, headers): - sys.stderr.write("ERROR: could not fetch {0}\n".format(url)) - sys.exit(-1) - AUTOCMAKE_GITHUB_URL = 'https://github.com/coderefinery/autocmake/raw/yaml/' # ------------------------------------------------------------------------------ -def replace(s, d): - from re import findall - if isinstance(s, str): - for var in findall(r"%\(([A-Za-z0-9_]*)\)", s): - s = s.replace("%({})".format(var), str(d[var])) - return s - - -def test_replace(): - assert replace('hey %(foo) ho %(bar)', - {'foo': 'hey', 'bar': 'ho'}) == 'hey hey ho ho' - -# ------------------------------------------------------------------------------ - - -def interpolate(d, d_map): - from collections import Mapping, Iterable - from copy import copy - for k, v in d.items(): - if isinstance(v, Mapping): - d[k] = interpolate(d[k], d_map) - elif isinstance(v, Iterable) and not isinstance(v, str): - l = [] - for x in v: - if isinstance(x, Mapping): - l.append(interpolate(x, d_map)) - else: - l.append(replace(x, d_map)) - d[k] = copy(l) - else: - d[k] = replace(d[k], d_map) - return d - - -def test_interpolate(): - d = {'foo': 'hey', - 'bar': 'ho', - 'one': 'hey %(foo) ho %(bar)', - 'two': {'one': 'hey %(foo) ho %(bar)', - 'two': 'raboof'}} - d_interpolated = {'foo': 'hey', - 'bar': 'ho', - 'one': 'hey hey ho ho', - 'two': {'one': 'hey hey ho ho', - 'two': 'raboof'}} - assert interpolate(d, d) == d_interpolated - d2 = {'modules': [{'fc': [{'source': '%(url_root)fc_optional.cmake'}]}], 'url_root': 'downloaded/downloaded_'} - d2_interpolated = {'modules': [{'fc': [{'source': 'downloaded/downloaded_fc_optional.cmake'}]}], 'url_root': 'downloaded/downloaded_'} - assert interpolate(d2, d2) == d2_interpolated - - -# ------------------------------------------------------------------------------ - - -def fetch_url(src, dst): - """ - Fetch file from URL src and save it to dst. - """ - dirname = os.path.dirname(dst) - if dirname != '': - if not os.path.isdir(dirname): - os.makedirs(dirname) - - opener = URLopener() - opener.retrieve(src, dst) - -# ------------------------------------------------------------------------------ - - -def parse_yaml(stream, override={}): - import yaml - - try: - config = yaml.load(stream, yaml.SafeLoader) - except yaml.YAMLError as exc: - print(exc) - sys.exit(-1) - - for k in config: - if k in override: - config[k] = override[k] - - config = interpolate(config, config) - return config - -# ------------------------------------------------------------------------------ - - def print_progress_bar(text, done, total, width): """ Print progress bar. @@ -349,6 +242,7 @@ def fetch_modules(config, relative_path): be included in CMakeLists.txt. """ from collections import Iterable + from autocmake.http import fetch_url download_directory = 'downloaded' if not os.path.exists(download_directory): @@ -431,6 +325,9 @@ def main(argv): """ Main function. """ + from autocmake.parse_yaml import parse_yaml + from autocmake.http import fetch_url + if len(argv) != 2: sys.stderr.write("\nYou can update a project in two steps.\n\n") sys.stderr.write("Step 1: Update or create infrastructure files\n") @@ -464,6 +361,7 @@ def main(argv): for f in ['autocmake/configure.py', 'autocmake/external/docopt.py', 'autocmake/__init__.py', + 'autocmake/interpolate.py', 'update.py']: print('- fetching {0}'.format(f)) fetch_url( @@ -535,135 +433,5 @@ def make_executable(path): # ------------------------------------------------------------------------------ -def parse_cmake_module(s_in, override={}): - from collections import Mapping, Iterable - - parsed_config = collections.defaultdict(lambda: None) - - if 'autocmake.yml configuration::' not in s_in: - return parsed_config - - s_out = [] - is_rst_line = False - for line in s_in.split('\n'): - if is_rst_line: - if len(line) > 0: - if line[0] != '#': - is_rst_line = False - else: - is_rst_line = False - if is_rst_line: - s_out.append(line[2:]) - if '#.rst:' in line: - is_rst_line = True - - autocmake_entry = '\n'.join(s_out).split('autocmake.yml configuration::')[1] - autocmake_entry = autocmake_entry.replace('\n ', '\n') - - buf = StringIO(autocmake_entry) - config = parse_yaml(buf, override) - - for k, v in config.items(): - if isinstance(v, Iterable) and not isinstance(v, str): - parsed_config[k] = [x for x in config[k]] - else: - parsed_config[k] = [config[k]] - - return parsed_config - -# ------------------------------------------------------------------------------ - - -def test_parse_cmake_module(): - - s = r'''#.rst: -# -# Foo ... -# -# autocmake.yml configuration:: -# -# docopt: -# - "--cxx= C++ compiler [default: g++]." -# - "--extra-cxx-flags= Extra C++ compiler flags [default: '']." -# export: "'CXX={0}'.format(arguments['--cxx'])" -# define: "'-DEXTRA_CXXFLAGS=\"{0}\"'.format(arguments['--extra-cxx-flags'])" - -enable_language(CXX) - -if(NOT DEFINED CMAKE_C_COMPILER_ID) - message(FATAL_ERROR "CMAKE_C_COMPILER_ID variable is not defined!") -endif()''' - - parsed_config = parse_cmake_module(s) - assert parsed_config['docopt'] == ["--cxx= C++ compiler [default: g++].", "--extra-cxx-flags= Extra C++ compiler flags [default: '']."] - - -def test_parse_cmake_module_no_key(): - - s = '''#.rst: -# -# Foo ... -# -# Bar ... - -enable_language(CXX) - -if(NOT DEFINED CMAKE_C_COMPILER_ID) - message(FATAL_ERROR "CMAKE_C_COMPILER_ID variable is not defined!") -endif()''' - - parsed_config = parse_cmake_module(s) - assert parsed_config['docopt'] is None - - -def test_parse_cmake_module_interpolate(): - - s = r'''#.rst: -# -# Foo ... -# -# autocmake.yml configuration:: -# -# major: 1 -# minor: 2 -# patch: 3 -# a: v%(major) -# b: v%(minor) -# c: v%(patch) - -enable_language(CXX)''' - - parsed_config = parse_cmake_module(s) - assert parsed_config['a'] == ['v1'] - assert parsed_config['b'] == ['v2'] - assert parsed_config['c'] == ['v3'] - - -def test_parse_cmake_module_override(): - - s = r'''#.rst: -# -# Foo ... -# -# autocmake.yml configuration:: -# -# major: 1 -# minor: 2 -# patch: 3 -# a: v%(major) -# b: v%(minor) -# c: v%(patch) - -enable_language(CXX)''' - - d = {'minor': 4} - parsed_config = parse_cmake_module(s, d) - assert parsed_config['a'] == ['v1'] - assert parsed_config['b'] == ['v4'] - assert parsed_config['c'] == ['v3'] - -# ------------------------------------------------------------------------------ - - if __name__ == '__main__': main(sys.argv)