golangconfig: Initial implementation of Package Control dependency Documentation within docs/development.md explains how to install for development and testing purposes. Once fully setup, the API is accessible by running "import golangconfig" in the Sublime Text console. Change-Id: I314a4b6aa4c7f951116322e0abd163d379af1f36
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfaf73f --- /dev/null +++ b/.gitignore
@@ -0,0 +1,4 @@ +*.pyc +__pycache__ +dev/coverage_reports/ +dev/mock_fs/ \ No newline at end of file
diff --git a/.pep8 b/.pep8 new file mode 100644 index 0000000..79a16af --- /dev/null +++ b/.pep8
@@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 \ No newline at end of file
diff --git a/.sublime-dependency b/.sublime-dependency new file mode 100644 index 0000000..9a03714 --- /dev/null +++ b/.sublime-dependency
@@ -0,0 +1 @@ +10 \ No newline at end of file
diff --git a/Main.sublime-menu b/Main.sublime-menu new file mode 100644 index 0000000..df36dd0 --- /dev/null +++ b/Main.sublime-menu
@@ -0,0 +1,29 @@ +[ + { + "id": "preferences", + "children": + [ + { + "caption": "Package Settings", + "mnemonic": "P", + "id": "package-settings", + "children": + [ + { + "caption": "Golang Config", + "children": + [ + { + "command": "open_file", "args": + { + "file": "${packages}/User/golang.sublime-settings" + }, + "caption": "Settings – User" + } + ] + } + ] + } + ] + } +]
diff --git a/all/golangconfig.py b/all/golangconfig.py new file mode 100644 index 0000000..a59386d --- /dev/null +++ b/all/golangconfig.py
@@ -0,0 +1,618 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os +import threading +import sys +import shellenv +import sublime + +if sys.version_info < (3,): + str_cls = unicode # noqa + py2 = True +else: + str_cls = str + py2 = False + + +__version__ = '1.0.0-beta' +__version_info__ = (1, 0, 0, 'beta') + + +# The sublime.platform() function will not be available in ST3 upon initial +# import, so we determine the platform via the sys.platform value. We cache +# the value here to prevent extra IPC calls between plugin_host and +# sublime_text in ST3. +_platform = { + 'win32': 'windows', + 'darwin': 'osx' +}.get(sys.platform, 'linux') + + +# A special value object to detect if a setting was not found, versus a setting +# explicitly being set to null/None in a settings file. We can't use a Python +# object here because the value is serialized to json via the ST API. Byte +# strings end up turning into an array of integers in ST3. +_NO_VALUE = '\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F' + + +class EnvVarError(EnvironmentError): + + """ + An error occurred finding one or more required environment variables + """ + + missing = None + + +class ExecutableError(EnvironmentError): + + """ + An error occurred locating the executable requested + """ + + name = None + dirs = None + + +def debug_enabled(): + """ + Checks to see if the "debug" setting is true + + :raises: + RuntimeError + When the function is called from any thread but the UI thread + + :return: + A boolean - if debug is enabled + """ + + # The Sublime Text API is not threadsafe in ST2, so we + # double check here to prevent crashes + if not isinstance(threading.current_thread(), threading._MainThread): + raise RuntimeError('golangconfig.setting_value() must be called from the main thread') + + value = sublime.load_settings('golang.sublime-settings').get('debug') + return False if value == '0' else bool(value) + + +def subprocess_info(executable_name, required_vars, optional_vars=None, view=None, window=None): + """ + Gathers and formats information necessary to use subprocess.Popen() to + run one of the go executables, with details pulled from setting_value() and + executable_path(). + + Ensures that the executable path and env dictionary are properly encoded for + Sublime Text 2, where byte strings are necessary. + + :param executable_name: + A unicode string of the executable to locate, e.g. "go" or "gofmt" + + :param required_vars: + A list of unicode strings of the environment variables that are + required, e.g. "GOPATH". Obtains values from setting_value(). + + :param optional_vars: + A list of unicode strings of the environment variables that are + optional, but should be pulled from setting_value() if available - e.g. + "GOOS", "GOARCH". Obtains values from setting_value(). + + :param view: + A sublime.View object to use in finding project-specific settings. This + should be passed whenever available. + + :param window: + A sublime.Window object to use in finding project-specific settings. + This should be passed whenever available. + + :raises: + RuntimeError + When the function is called from any thread but the UI thread + TypeError + When any of the parameters are of the wrong type + golangconfig.ExecutableError + When the executable requested could not be located. The .name + attribute contains the name of the executable that could not be + located. The .dirs attribute contains a list of unicode strings + of the directories searched. + golangconfig.EnvVarError + When one or more required_vars are not available. The .missing + attribute will be a list of the names of missing environment + variables. + + :return: + A two-element tuple. + + - [0] A unicode string (byte string for ST2) of the path to the executable + - [1] A dict to pass to the env parameter of subprocess.Popen() + """ + + path, _ = executable_path(executable_name, view=view, window=window) + if path is None: + name = executable_name + if sys.platform == 'win32': + name += '.exe' + dirs = [] + settings_path, _ = _get_most_specific_setting('PATH', view=view, window=window) + if settings_path and settings_path != _NO_VALUE: + dirs.extend(settings_path.split(os.pathsep)) + _, shell_dirs = shellenv.get_path() + for shell_dir in shell_dirs: + if shell_dir not in dirs: + dirs.append(shell_dir) + exception = ExecutableError( + 'The executable "%s" could not be located in any of the following locations: "%s"' % + ( + name, + '", "'.join(dirs) + ) + ) + exception.name = name + exception.dirs = dirs + raise exception + + path = shellenv.path_encode(path) + + _, env = shellenv.get_env(for_subprocess=True) + + var_groups = [required_vars] + if optional_vars: + var_groups.append(optional_vars) + + missing_vars = [] + + for var_names in var_groups: + for var_name in var_names: + value, _ = setting_value(var_name, view=view, window=window) + var_key = var_name + + if value is not None: + value = str_cls(value) + value = shellenv.env_encode(value) + var_key = shellenv.env_encode(var_key) + + if value is None: + if var_key in env: + del env[var_key] + continue + + env[var_key] = value + + for required_var in required_vars: + var_key = shellenv.env_encode(required_var) + if var_key not in env: + missing_vars.append(required_var) + + if missing_vars: + missing_vars = sorted(missing_vars, key=lambda s: s.lower()) + exception = EnvVarError( + 'The following environment variable%s currently unset: %s' % + ( + 's are' if len(missing_vars) > 1 else ' is', + ', '.join(missing_vars) + ) + ) + exception.missing = missing_vars + raise exception + + encoded_goroot = shellenv.env_encode('GOROOT') + if encoded_goroot in env: + unicode_sep = shellenv.path_decode(os.sep) + name = executable_name + if sys.platform == 'win32': + name += '.exe' + relative_executable_path = shellenv.path_encode('bin%s%s' % (unicode_sep, name)) + goroot_executable_path = os.path.join(env[encoded_goroot], relative_executable_path) + if goroot_executable_path != path: + print( + 'golangconfig: warning - binary %s was found at "%s", which is not inside of the GOROOT "%s"' % + ( + executable_name, + path, + shellenv.path_decode(env[encoded_goroot]) + ) + ) + + return (path, env) + + +def setting_value(setting_name, view=None, window=None): + """ + Returns the user's setting for a specific variable, such as GOPATH or + GOROOT. Supports global and per-platform settings. Finds settings by + looking in: + + 1. If a project is open, the project settings + 2. The global golang.sublime-settings file + 3. The user's environment variables, as defined by their login shell + + If the setting is a known name, e.g. GOPATH or GOROOT, the value will be + checked to ensure the path exists. + + :param setting_name: + A unicode string of the setting to retrieve + + :param view: + A sublime.View object to use in finding project-specific settings. This + should be passed whenever available. + + :param window: + A sublime.Window object to use in finding project-specific settings. + This should be passed whenever available. + + :raises: + RuntimeError + When the function is called from any thread but the UI thread + TypeError + When any of the parameters are of the wrong type + + :return: + A two-element tuple. + + If no setting was found, the return value will be: + + - [0] None + - [1] None + + If a setting was found, the return value will be: + + - [0] The setting value + - [1] The source of the setting, a unicode string: + - "project file (os-specific)" + - "golang.sublime-settings (os-specific)" + - "project file" + - "golang.sublime-settings" + - A unicode string of the path to the user's login shell + + The second element of the tuple is intended to be used in the display + of debugging information to end users. + """ + + _require_unicode('setting_name', setting_name) + _check_view_window(view, window) + + setting, source = _get_most_specific_setting(setting_name, view, window) + + if setting == _NO_VALUE: + setting = None + source = None + + shell, env = shellenv.get_env() + if setting_name in env: + source = shell + setting = env[setting_name] + + if setting_name not in set(['GOPATH', 'GOROOT']): + return (setting, source) + + # We add some extra processing here for known settings to improve the + # user experience, especially around debugging + is_str = isinstance(setting, str_cls) + if is_str: + if os.path.exists(setting): + return (setting, source) + + if debug_enabled(): + print( + 'golangconfig: the value for %s from %s - "%s" - does not exist on the filesystem' % + ( + setting_name, + source, + setting + ) + ) + + elif debug_enabled(): + _debug_unicode_string(setting_name, setting, source) + + return (None, None) + + +def executable_path(executable_name, view=None, window=None): + """ + Uses the user's Sublime Text settings and then PATH environment variable + as set by their login shell to find a go executable + + :param name: + The name of the binary to find - a unicode string of "go", "gofmt" or + "godoc" + + :param view: + A sublime.View object to use in finding project-specific settings. This + should be passed whenever available. + + :param window: + A sublime.Window object to use in finding project-specific settings. + This should be passed whenever available. + + :raises: + RuntimeError + When the function is called from any thread but the UI thread + TypeError + When any of the parameters are of the wrong type + + :return: + A 2-element tuple. + + If the executable was not found, the return value will be: + + - [0] None + - [1] None + + If the exeutable was found, the return value will be: + + - [0] A unicode string of the full path to the executable + - [1] A unicode string of the source of the PATH value + - "project file (os-specific)" + - "golang.sublime-settings (os-specific)" + - "project file" + - "golang.sublime-settings" + - A unicode string of the path to the user's login shell + + The second element of the tuple is intended to be used in the display + of debugging information to end users. + """ + + _require_unicode('executable_name', executable_name) + _check_view_window(view, window) + + executable_suffix = '.exe' if sys.platform == 'win32' else '' + suffixed_name = executable_name + executable_suffix + + setting, source = _get_most_specific_setting('PATH', view, window) + if setting is not _NO_VALUE: + is_str = isinstance(setting, str_cls) + if not is_str: + if debug_enabled(): + _debug_unicode_string('PATH', setting, source) + else: + for dir_ in setting.split(os.pathsep): + possible_executable_path = os.path.join(dir_, suffixed_name) + if _check_executable(possible_executable_path, source, setting): + return (possible_executable_path, source) + + if debug_enabled(): + print( + 'golangconfig: binary %s not found in PATH from %s - "%s"' % + ( + executable_name, + source, + setting + ) + ) + + shell, path_dirs = shellenv.get_path() + for dir_ in path_dirs: + possible_executable_path = os.path.join(dir_, suffixed_name) + if _check_executable(possible_executable_path, shell, os.pathsep.join(path_dirs)): + return (possible_executable_path, shell) + + if debug_enabled(): + print( + 'golangconfig: binary %s not found in PATH from %s - "%s"' % + ( + executable_name, + shell, + os.pathsep.join(path_dirs) + ) + ) + + return (None, None) + + +def _get_most_specific_setting(name, view, window): + """ + Looks up a setting in the following order: + + 1. View settings, looking inside of the "osx", "windows" or "linux" key + based on the OS that Sublime Text is running on. These settings are from + a project file. + 2. Window settings (ST3 only), looking inside of the "osx", "windows" or + "linux" key based on the OS that Sublime Text is running on. These + settings are from a project file. + 3. golang.sublime-settings, looking inside of the "osx", "windows" or + "linux" key based on the OS that Sublime Text is running on. + 4. The view settings. These settings are from a project file. + 5. The window settings (ST3 only). These settings are from a project file. + 6. golang.sublime-settings + + :param name: + A unicode string of the setting to fetch + + :param view: + A sublime.View object to use in finding project-specific settings. This + should be passed whenever available. + + :param window: + A sublime.Window object to use in finding project-specific settings. + This should be passed whenever available. + + :return: + A two-element tuple. + + If no setting was found, the return value will be: + + - [0] golangconfig._NO_VALUE + - [1] None + + If a setting was found, the return value will be: + + - [0] The setting value + - [1] A unicode string of the source: + - "project file (os-specific)" + - "golang.sublime-settings (os-specific)" + - "project file" + - "golang.sublime-settings" + """ + + # The Sublime Text API is not threadsafe in ST2, so we + # double check here to prevent crashes + if not isinstance(threading.current_thread(), threading._MainThread): + raise RuntimeError('golangconfig.setting_value() must be called from the main thread') + + if view is not None and not isinstance(view, sublime.View): + raise TypeError('view must be an instance of sublime.View, not %s' % _type_name(view)) + + if window is not None and not isinstance(window, sublime.Window): + raise TypeError('window must be an instance of sublime.Window, not %s' % _type_name(window)) + + st_settings = sublime.load_settings('golang.sublime-settings') + + view_settings = view.settings().get('golang', {}) if view else {} + + if view and not window: + window = view.window() + + window_settings = {} + if window: + if sys.version_info >= (3,): + window_settings = window.project_data().get('settings', {}).get('golang', {}) + elif not view and window.active_view(): + window_settings = window.active_view().settings().get('golang', {}) + + settings_objects = [ + (view_settings, 'project file'), + (window_settings, 'project file'), + (st_settings, 'golang.sublime-settings'), + ] + + for settings_object, source in settings_objects: + platform_settings = settings_object.get(_platform, _NO_VALUE) + if platform_settings == _NO_VALUE: + continue + if not isinstance(platform_settings, dict): + continue + if platform_settings.get(name, _NO_VALUE) != _NO_VALUE: + return (platform_settings.get(name), source + ' (os-specific)') + + for settings_object, source in settings_objects: + result = settings_object.get(name, _NO_VALUE) + if result != _NO_VALUE: + return (settings_object.get(name), source) + + return (_NO_VALUE, None) + + +def _require_unicode(name, value): + """ + Requires that a parameter be a unicode string + + :param name: + A unicode string of the parameter name + + :param value: + The parameter value + + :raises: + TypeError + When the value is not a unicode string + """ + + if not isinstance(value, str_cls): + raise TypeError('%s must be a unicode string, not %s' % (name, _type_name(value))) + + +def _check_view_window(view, window): + """ + Ensures that the view and window parameters to a function are suitable + objects for our purposes. There is not a strict check for type to allow for + mocking during testing. + + :param view: + The view parameter to check + + :param window: + The window parameter to check + + :raises: + TypeError + When the view or window parameters are not of the appropriate type + """ + + if view is not None: + if not isinstance(view, sublime.View): + raise TypeError('view must be an instance of sublime.View, not %s' % _type_name(view)) + + if window is not None: + if not isinstance(window, sublime.Window) and sys.version_info >= (3,): + raise TypeError('window must be an instance of sublime.Window, not %s' % _type_name(window)) + + +def _type_name(value): + """ + :param value: + The value to get the type name of + + :return: + A unicode string of the name of the value's type + """ + + value_cls = value.__class__ + value_module = value_cls.__module__ + if value_module in set(['builtins', '__builtin__']): + return value_cls.__name__ + + return '%s.%s' % (value_module, value_cls.__name__) + + +def _debug_unicode_string(name, value, source): + """ + Displays a debug message to the console if the value is not a unicode string + + :param name: + A unicode string of the name of the setting + + :param value: + The setting value to check + + :param source: + A unicode string of the source of the setting + """ + + if value is not None and not isinstance(value, str_cls): + print( + 'golangconfig: the value for %s from %s is not a string, but instead a %s' % + ( + name, + source, + _type_name(value) + ) + ) + + +def _check_executable(possible_executable_path, source, setting): + """ + Checks to see if a path to an executable exists and that it is, in fact, + executable. Will display debug info if the path exists, but is not + executable. + + :param possible_executable_path: + A unicode string of the full file path to the executable + + :param source: + A unicode string of the source of the setting + + :param setting: + A unicode string of the PATH value that the executable was found in + + :return: + A boolean - if the possible_executable_path is a file that is executable + """ + + if os.path.exists(possible_executable_path): + is_executable = os.path.isfile(possible_executable_path) and os.access(possible_executable_path, os.X_OK) + if is_executable: + return True + + if debug_enabled(): + executable_name = os.path.basename(possible_executable_path) + print( + 'golangconfig: binary %s found in PATH from %s - "%s" - is not executable' % + ( + executable_name, + source, + setting + ) + ) + + return False
diff --git a/dev/__init__.py b/dev/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/dev/__init__.py
diff --git a/dev/api_docs.py b/dev/api_docs.py new file mode 100644 index 0000000..430bf4b --- /dev/null +++ b/dev/api_docs.py
@@ -0,0 +1,429 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +""" +This script is used to extract markdown API docs from Python docstrings of code +that is part of the golangconfig library. +""" + +# This file is Copyright 2015, Will Bond <will@wbond.net>, licensed to Google +# under the terms of the Google Inbound Service Agreement, signed August 15 2015 + +import re +import os +import ast +import _ast +import textwrap + +import CommonMark +from collections import OrderedDict + + +cur_dir = os.path.dirname(__file__) +project_dir = os.path.abspath(os.path.join(cur_dir, '..')) +docs_dir = os.path.join(project_dir, 'docs') +module_name = 'golangconfig' + + +# Maps a markdown document to a Python source file to look in for +# class/method/function docstrings +MD_SOURCE_MAP = { + 'docs/package_developer.md': ['all/golangconfig.py'], +} + +# A search/replace dictionary to modify docstring contents before generating +# markdown from them +definition_replacements = {} + + +def _get_func_info(docstring, def_lineno, code_lines, prefix): + """ + Extracts the function signature and description of a Python function + + :param docstring: + A unicode string of the docstring for the function + + :param def_lineno: + An integer line number that function was defined on + + :param code_lines: + A list of unicode string lines from the source file the function was + defined in + + :param prefix: + A prefix to prepend to all output lines + + :return: + A 2-element tuple: + + - [0] A unicode string of the function signature with a docstring of + parameter info + - [1] A markdown snippet of the function description + """ + + definition = code_lines[def_lineno - 1] + definition = definition.strip().rstrip(':') + + description = '' + found_colon = False + + params = '' + + for line in docstring.splitlines(): + if line and line[0] == ':': + found_colon = True + if not found_colon: + if description: + description += '\n' + description += line + else: + if params: + params += '\n' + params += line + + description = description.strip() + description_md = '' + if description: + description_md = "%s%s" % (prefix, description.replace("\n", "\n" + prefix)) + description_md = re.sub('\n>(\\s+)\n', '\n>\n', description_md) + + params = params.strip() + if params: + definition += (':\n%s """\n%s ' % (prefix, prefix)) + definition += params.replace('\n', '\n%s ' % prefix) + definition += ('\n%s """' % prefix) + definition = re.sub('\n>(\\s+)\n', '\n>\n', definition) + + for search, replace in definition_replacements.items(): + definition = definition.replace(search, replace) + + return (definition, description_md) + + +def _find_sections(md_ast, sections, last, last_class, total_lines=None): + """ + Walks through a CommonMark AST to find section headers that delineate + content that should be updated by this script + + :param md_ast: + The AST of the markdown document + + :param sections: + A dict to store the start and end lines of a section. The key will be + a two-element tuple of the section type ("class", "function", + "method" or "attribute") and identifier. The values are a two-element + tuple of the start and end line number in the markdown document of the + section. + + :param last: + A dict containing information about the last section header seen. + Includes the keys "type_name", "identifier", "start_line". + + :param last_class: + A unicode string of the name of the last class found - used when + processing methods and attributes. + + :param total_lines: + An integer of the total number of lines in the markdown document - + used to work around a bug in the API of the Python port of CommonMark + """ + + for child in md_ast.children: + if child.t == 'ATXHeader': + + if child.level in set([3, 5]) and len(child.inline_content) == 2: + first = child.inline_content[0] + second = child.inline_content[1] + if first.t != 'Code': + continue + if second.t != 'Str': + continue + type_name = second.c.strip() + identifier = first.c.strip().replace('()', '').lstrip('.') + + if last: + sections[(last['type_name'], last['identifier'])] = (last['start_line'], child.start_line - 1) + last.clear() + + if type_name == 'function': + if child.level != 3: + continue + + if type_name == 'class': + if child.level != 3: + continue + last_class.append(identifier) + + if type_name in set(['method', 'attribute']): + if child.level != 5: + continue + identifier = last_class[-1] + '.' + identifier + + last.update({ + 'type_name': type_name, + 'identifier': identifier, + 'start_line': child.start_line, + }) + + elif child.t == 'BlockQuote': + find_sections(child, sections, last, last_class) + + if last: + sections[(last['type_name'], last['identifier'])] = (last['start_line'], total_lines) + +find_sections = _find_sections + + +def walk_ast(node, code_lines, sections, md_chunks): + """ + A callback used to walk the Python AST looking for classes, functions, + methods and attributes. Generates chunks of markdown markup to replace + the existing content. + + :param node: + An _ast module node object + + :param code_lines: + A list of unicode strings - the source lines of the Python file + + :param sections: + A dict of markdown document sections that need to be updated. The key + will be a two-element tuple of the section type ("class", "function", + "method" or "attribute") and identifier. The values are a two-element + tuple of the start and end line number in the markdown document of the + section. + + :param md_chunks: + A dict with keys from the sections param and the values being a unicode + string containing a chunk of markdown markup. + """ + + if isinstance(node, _ast.FunctionDef): + key = ('function', node.name) + if key not in sections: + return + + docstring = ast.get_docstring(node) + def_lineno = node.lineno + len(node.decorator_list) + + definition, description_md = _get_func_info(docstring, def_lineno, code_lines, '> ') + + md_chunk = textwrap.dedent(""" + ### `%s()` function + + > ```python + > %s + > ``` + > + %s + """).strip() % ( + node.name, + definition, + description_md + ) + "\n" + + md_chunks[key] = md_chunk + + elif isinstance(node, _ast.ClassDef): + if ('class', node.name) not in sections: + return + + for subnode in node.body: + if isinstance(subnode, _ast.FunctionDef): + node_id = node.name + '.' + subnode.name + + method_key = ('method', node_id) + is_method = method_key in sections + + attribute_key = ('attribute', node_id) + is_attribute = attribute_key in sections + + is_constructor = subnode.name == '__init__' + + if not is_constructor and not is_attribute and not is_method: + continue + + docstring = ast.get_docstring(subnode) + def_lineno = subnode.lineno + len(subnode.decorator_list) + + if not docstring: + continue + + if is_method or is_constructor: + definition, description_md = _get_func_info(docstring, def_lineno, code_lines, '> > ') + + if is_constructor: + key = ('class', node.name) + + class_docstring = ast.get_docstring(node) + class_description = textwrap.dedent(class_docstring).strip() + if class_description: + class_description_md = "> %s\n>" % (class_description.replace("\n", "\n> ")) + else: + class_description_md = '' + + md_chunk = textwrap.dedent(""" + ### `%s()` class + + %s + > ##### constructor + > + > > ```python + > > %s + > > ``` + > > + %s + """).strip() % ( + node.name, + class_description_md, + definition, + description_md + ) + + md_chunk = md_chunk.replace('\n\n\n', '\n\n') + + else: + key = method_key + + md_chunk = textwrap.dedent(""" + > + > ##### `.%s()` method + > + > > ```python + > > %s + > > ``` + > > + %s + """).strip() % ( + subnode.name, + definition, + description_md + ) + + if md_chunk[-5:] == '\n> >\n': + md_chunk = md_chunk[0:-5] + + else: + key = attribute_key + + description = textwrap.dedent(docstring).strip() + description_md = "> > %s" % (description.replace("\n", "\n> > ")) + + md_chunk = textwrap.dedent(""" + > + > ##### `.%s` attribute + > + %s + """).strip() % ( + subnode.name, + description_md + ) + + md_chunks[key] = md_chunk.rstrip() + + elif isinstance(node, _ast.If): + for subast in node.body: + walk_ast(subast, code_lines, sections, md_chunks) + for subast in node.orelse: + walk_ast(subast, code_lines, sections, md_chunks) + + +def run(): + """ + Looks through the docs/ dir and parses each markdown document, looking for + sections to update from Python docstrings. Looks for section headers in + the format: + + - ### `ClassName()` class + - ##### `.method_name()` method + - ##### `.attribute_name` attribute + - ### `function_name()` function + + The markdown content following these section headers up until the next + section header will be replaced by new markdown generated from the Python + docstrings of the associated source files. + + By default maps docs/{name}.md to {modulename}/{name}.py. Allows for + custom mapping via the MD_SOURCE_MAP variable. + """ + + print('Updating API docs...') + + md_files = [] + for root, _, filenames in os.walk(docs_dir): + for filename in filenames: + if not filename.endswith('.md'): + continue + md_files.append(os.path.join(root, filename)) + + parser = CommonMark.DocParser() + + for md_file in md_files: + md_file_relative = md_file[len(project_dir) + 1:] + if md_file_relative in MD_SOURCE_MAP: + py_files = MD_SOURCE_MAP[md_file_relative] + py_paths = [os.path.join(project_dir, py_file) for py_file in py_files] + else: + py_files = [os.path.basename(md_file).replace('.md', '.py')] + py_paths = [os.path.join(project_dir, module_name, py_files[0])] + + if not os.path.exists(py_paths[0]): + continue + + with open(md_file, 'rb') as f: + markdown = f.read().decode('utf-8') + + original_markdown = markdown + md_lines = list(markdown.splitlines()) + md_ast = parser.parse(markdown) + + last_class = [] + last = {} + sections = OrderedDict() + find_sections(md_ast, sections, last, last_class, markdown.count("\n") + 1) + + md_chunks = {} + + for index, py_file in enumerate(py_files): + py_path = py_paths[index] + + with open(os.path.join(py_path), 'rb') as f: + code = f.read().decode('utf-8') + module_ast = ast.parse(code, filename=py_file) + code_lines = list(code.splitlines()) + + for node in ast.iter_child_nodes(module_ast): + walk_ast(node, code_lines, sections, md_chunks) + + added_lines = 0 + + def _replace_md(key, sections, md_chunk, md_lines, added_lines): + start, end = sections[key] + start -= 1 + start += added_lines + end += added_lines + new_lines = md_chunk.split('\n') + added_lines += len(new_lines) - (end - start) + + # Ensure a newline above each class header + if start > 0 and md_lines[start][0:4] == '### ' and md_lines[start - 1][0:1] == '>': + added_lines += 1 + new_lines.insert(0, '') + + md_lines[start:end] = new_lines + return added_lines + + for key in sections: + if key not in md_chunks: + raise ValueError('No documentation found for %s' % key[1]) + added_lines = _replace_md(key, sections, md_chunks[key], md_lines, added_lines) + + markdown = '\n'.join(md_lines).strip() + '\n' + + if original_markdown != markdown: + with open(md_file, 'wb') as f: + f.write(markdown.encode('utf-8')) + + +if __name__ == '__main__': + run()
diff --git a/dev/mocks.py b/dev/mocks.py new file mode 100644 index 0000000..3085c70 --- /dev/null +++ b/dev/mocks.py
@@ -0,0 +1,241 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os +import sys +import shutil +import locale +import stat + +import golangconfig + +if sys.version_info < (3,): + from cStringIO import StringIO + str_cls = unicode # noqa +else: + from io import StringIO + str_cls = str + + +class SublimeViewMock(): + + _settings = None + _context = None + + def __init__(self, settings, context): + self._settings = settings + self._context = context + + def settings(self): + if self.window(): + # In Sublime Text, View objects inherit settings from the window/project + # unless they are explicitly set on the view, so we replicate that here + merged_golang_settings = self.window().project_data().get('settings', {}).get('golang', {}).copy() + merged_golang_settings.update(self._settings) + elif self._settings: + merged_golang_settings = self._settings.copy() + else: + merged_golang_settings = {} + return {'golang': merged_golang_settings} + + def window(self): + return self._context.window + + +class SublimeWindowMock(): + + _settings = None + _context = None + + def __init__(self, settings, context): + self._settings = settings + self._context = context + + def project_data(self): + return {'settings': {'golang': self._settings}} + + def active_view(self): + if self._context.view: + return self._context.view + return SublimeViewMock({}, self._context) + + +class ShellenvMock(): + + _env_encoding = locale.getpreferredencoding() if sys.platform == 'win32' else 'utf-8' + _fs_encoding = 'mbcs' if sys.platform == 'win32' else 'utf-8' + + _shell = None + _data = None + + def __init__(self, shell, data): + self._shell = shell + self._data = data + + def get_env(self, for_subprocess=False): + if not for_subprocess or sys.version_info >= (3,): + return (self._shell, self._data) + + shell = self._shell.encode(self._fs_encoding) + env = {} + for name, value in self._data.items(): + env[name.encode(self._env_encoding)] = value.encode(self._env_encoding) + + return (shell, env) + + def get_path(self): + return (self._shell, self._data.get('PATH', '').split(os.pathsep)) + + def env_encode(self, value): + if sys.version_info >= (3,): + return value + return value.encode(self._env_encoding) + + def path_encode(self, value): + if sys.version_info >= (3,): + return value + return value.encode(self._fs_encoding) + + def path_decode(self, value): + if sys.version_info >= (3,): + return value + return value.decode(self._fs_encoding) + + +class SublimeSettingsMock(): + + _values = None + + def __init__(self, values): + self._values = values + + def get(self, name, default=None): + return self._values.get(name, default) + + +class SublimeMock(): + + _settings = None + View = SublimeViewMock + Window = SublimeWindowMock + + def __init__(self, settings): + self._settings = SublimeSettingsMock(settings) + + def load_settings(self, basename): + return self._settings + + +class GolangConfigMock(): + + _shellenv = None + _sublime = None + _stdout = None + + _tempdir = None + + _shell = None + _env = None + _view_settings = None + _window_settings = None + _sublime_settings = None + + def __init__(self, shell, env, view_settings, window_settings, sublime_settings): + self._shell = shell + self._env = env + self._view_settings = view_settings + self._window_settings = window_settings + self._sublime_settings = sublime_settings + self._tempdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mock_fs') + if not os.path.exists(self._tempdir): + os.mkdir(self._tempdir) + + def replace_tempdir_env(self): + for key in self._env: + self._env[key] = self._env[key].replace( + '{tempdir}', + self.tempdir + os.sep + ) + + def replace_tempdir_view_settings(self): + self._replace_tempdir_settings(self._view_settings) + + def replace_tempdir_window_settings(self): + self._replace_tempdir_settings(self._window_settings) + + def replace_tempdir_sublime_settings(self): + self._replace_tempdir_settings(self._sublime_settings) + + def _replace_tempdir_settings(self, settings_dict): + if settings_dict: + for key in settings_dict: + if isinstance(settings_dict[key], str_cls): + settings_dict[key] = settings_dict[key].replace( + '{tempdir}', + self.tempdir + os.sep + ) + for platform in ['osx', 'windows', 'linux']: + if platform not in settings_dict: + continue + for key in settings_dict[platform]: + if isinstance(settings_dict[platform][key], str_cls): + settings_dict[platform][key] = settings_dict[platform][key].replace( + '{tempdir}', + self.tempdir + os.sep + ) + + def make_executable_files(self, executable_temp_files): + self.make_files(executable_temp_files) + for temp_file in executable_temp_files: + temp_file_path = os.path.join(self.tempdir, temp_file) + st = os.stat(temp_file_path) + os.chmod(temp_file_path, st.st_mode | stat.S_IEXEC) + + def make_files(self, temp_files): + for temp_file in temp_files: + temp_file_path = os.path.join(self.tempdir, temp_file) + temp_file_dir = os.path.dirname(temp_file_path) + if not os.path.exists(temp_file_dir): + os.makedirs(temp_file_dir) + with open(temp_file_path, 'a'): + pass + + def make_dirs(self, temp_dirs): + for temp_dir in temp_dirs: + temp_dir_path = os.path.join(self.tempdir, temp_dir) + if not os.path.exists(temp_dir_path): + os.makedirs(temp_dir_path) + + @property + def view(self): + if self._view_settings is None: + return None + return SublimeViewMock(self._view_settings, self) + + @property + def window(self): + if self._window_settings is None: + return None + return SublimeWindowMock(self._window_settings, self) + + @property + def tempdir(self): + return self._tempdir + + def __enter__(self): + self._shellenv = golangconfig.shellenv + golangconfig.shellenv = ShellenvMock(self._shell, self._env) + self._sublime = golangconfig.sublime + golangconfig.sublime = SublimeMock(self._sublime_settings) + self._stdout = sys.stdout + sys.stdout = StringIO() + return self + + def __exit__(self, exc_type, exc_value, traceback): + golangconfig.shellenv = self._shellenv + golangconfig.sublime = self._sublime + temp_stdout = sys.stdout + sys.stdout = self._stdout + print(temp_stdout.getvalue(), end='') + if self._tempdir and os.path.exists(self._tempdir): + shutil.rmtree(self._tempdir)
diff --git a/dev/reloader.py b/dev/reloader.py new file mode 100644 index 0000000..69f9f9d --- /dev/null +++ b/dev/reloader.py
@@ -0,0 +1,13 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + + +if sys.version_info >= (3,): + from imp import reload + +if 'golangconfig.dev.mocks' in sys.modules: + reload(sys.modules['golangconfig.dev.mocks']) +if 'golangconfig' in sys.modules: + reload(sys.modules['golangconfig'])
diff --git a/dev/tests.py b/dev/tests.py new file mode 100644 index 0000000..0e6e3aa --- /dev/null +++ b/dev/tests.py
@@ -0,0 +1,576 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import unittest + +import sys +import os + +if sys.version_info < (3,): + str_cls = unicode # noqa +else: + str_cls = str + +import shellenv +import golangconfig +from .mocks import GolangConfigMock +from .unittest_data import data, data_class + + +class CustomString(): + + value = None + + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value + + def __unicode__(self): + return self.__str__() + + +@data_class +class GolangconfigTests(unittest.TestCase): + + @staticmethod + def subprocess_info_data(): + return ( + ( + 'basic_shell', + '/bin/bash', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}gopath', + }, + None, + None, + {'debug': True}, + ['usr/bin/go'], + ['gopath/'], + 'go', + ['GOPATH'], + None, + ( + '{tempdir}usr/bin/go', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}gopath', + } + ), + ), + ( + 'view_setting_override', + '/bin/bash', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}gopath', + }, + {'GOPATH': '{tempdir}custom/gopath', 'GOOS': 'windows'}, + None, + {'debug': True}, + ['usr/bin/go'], + ['custom/gopath/'], + 'go', + ['GOPATH'], + None, + ( + '{tempdir}usr/bin/go', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}custom/gopath', + } + ), + ), + ( + 'view_setting_override_optional_missing', + '/bin/bash', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}gopath', + }, + {'GOPATH': '{tempdir}custom/gopath'}, + None, + {'debug': True}, + ['usr/bin/go'], + ['custom/gopath/'], + 'go', + ['GOPATH'], + ['GOOS'], + ( + '{tempdir}usr/bin/go', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}custom/gopath', + } + ), + ), + ( + 'view_setting_override_optional_present', + '/bin/bash', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}gopath', + }, + {'GOPATH': '{tempdir}custom/gopath', 'GOOS': 'windows'}, + None, + {'debug': True}, + ['usr/bin/go'], + ['custom/gopath/'], + 'go', + ['GOPATH'], + ['GOOS'], + ( + '{tempdir}usr/bin/go', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}custom/gopath', + 'GOOS': 'windows', + } + ), + ), + ( + 'view_setting_unset', + '/bin/bash', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}gopath', + 'GOOS': 'windows' + }, + {'GOPATH': '{tempdir}custom/gopath', 'GOOS': None}, + None, + {'debug': True}, + ['usr/bin/go'], + ['custom/gopath/'], + 'go', + ['GOPATH'], + ['GOOS'], + ( + '{tempdir}usr/bin/go', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}custom/gopath', + } + ), + ), + ( + 'no_executable', + '/bin/bash', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}gopath', + }, + {'GOPATH': '{tempdir}custom/gopath'}, + None, + {'debug': True, 'PATH': '{tempdir}usr/local/bin'}, + [], + ['custom/gopath/'], + 'go', + ['GOPATH'], + None, + golangconfig.ExecutableError + ), + ( + 'env_var_missing', + '/bin/bash', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin', + 'GOPATH': '{tempdir}gopath', + }, + {'GOPATH': '{tempdir}custom/gopath'}, + None, + {'debug': True}, + ['bin/go'], + ['custom/gopath/'], + 'go', + ['GOPATH', 'GOROOT'], + None, + golangconfig.EnvVarError + ), + ) + + @data('subprocess_info_data', True) + def subprocess_info(self, shell, env, view_settings, window_settings, sublime_settings, + executable_temp_files, temp_dirs, executable_name, required_vars, optional_vars, + expected_result): + + with GolangConfigMock(shell, env, view_settings, window_settings, sublime_settings) as mock_context: + + mock_context.replace_tempdir_env() + mock_context.replace_tempdir_view_settings() + mock_context.replace_tempdir_window_settings() + mock_context.replace_tempdir_sublime_settings() + + mock_context.make_executable_files(executable_temp_files) + mock_context.make_dirs(temp_dirs) + + if isinstance(expected_result, tuple): + tempdir = mock_context.tempdir + os.sep + executable_path = expected_result[0].replace('{tempdir}', tempdir) + executable_path = shellenv.path_encode(executable_path) + + env_vars = {} + for name, value in expected_result[1].items(): + value = value.replace('{tempdir}', tempdir) + name = shellenv.env_encode(name) + value = shellenv.env_encode(value) + env_vars[name] = value + + expected_result = (executable_path, env_vars) + + self.assertEquals( + expected_result, + golangconfig.subprocess_info( + executable_name, + required_vars, + optional_vars=optional_vars, + view=mock_context.view, + window=mock_context.window + ) + ) + self.assertEqual('', sys.stdout.getvalue()) + + else: + def do_test(): + golangconfig.subprocess_info( + executable_name, + required_vars, + optional_vars=optional_vars, + view=mock_context.view, + window=mock_context.window + ) + self.assertRaises(expected_result, do_test) + + @staticmethod + def executable_path_data(): + return ( + ( + 'basic_shell', + '/bin/bash', + { + 'PATH': '{tempdir}bin:{tempdir}usr/bin' + }, + None, + None, + {'debug': True}, + ['bin/go'], + [], + None, + ('{tempdir}bin/go', '/bin/bash'), + ), + ( + 'basic_view_settings', + '/bin/bash', + { + 'PATH': '{tempdir}bin' + }, + {'PATH': '{tempdir}usr/bin:{tempdir}usr/local/bin'}, + {}, + {}, + ['usr/local/bin/go'], + ['usr/bin/go'], + None, + ('{tempdir}usr/local/bin/go', 'project file'), + ), + ( + 'basic_view_settings_debug', + '/bin/bash', + { + 'PATH': '{tempdir}bin' + }, + {'PATH': '{tempdir}usr/bin:{tempdir}usr/local/bin'}, + {}, + {'debug': True}, + ['usr/local/bin/go'], + ['usr/bin/go'], + 'is not executable', + ('{tempdir}usr/local/bin/go', 'project file'), + ), + ( + 'basic_view_settings_none_found', + '/bin/bash', + { + 'PATH': '{tempdir}bin' + }, + {'PATH': '{tempdir}usr/bin:{tempdir}usr/local/bin'}, + {}, + {'debug': True}, + [], + ['usr/bin/go'], + 'is not executable', + (None, None), + ), + ) + + @data('executable_path_data', True) + def executable_path(self, shell, env, view_settings, window_settings, sublime_settings, + executable_temp_files, non_executable_temp_files, expected_debug, expected_result): + + with GolangConfigMock(shell, env, view_settings, window_settings, sublime_settings) as mock_context: + + mock_context.replace_tempdir_env() + mock_context.replace_tempdir_view_settings() + mock_context.replace_tempdir_window_settings() + + mock_context.make_executable_files(executable_temp_files) + mock_context.make_files(non_executable_temp_files) + + if expected_result[0]: + tempdir = mock_context.tempdir + os.sep + expected_result = (expected_result[0].replace('{tempdir}', tempdir), expected_result[1]) + + self.assertEquals( + expected_result, + golangconfig.executable_path('go', mock_context.view, mock_context.window) + ) + if expected_debug is None: + self.assertEqual('', sys.stdout.getvalue()) + else: + self.assertTrue(expected_debug in sys.stdout.getvalue()) + + def test_executable_path_path_not_string(self): + shell = '/bin/bash' + env = { + 'PATH': '/bin' + } + view_settings = { + 'PATH': 1 + } + with GolangConfigMock(shell, env, view_settings, None, {'debug': True}) as mock_context: + self.assertEquals((None, None), golangconfig.executable_path('go', mock_context.view, mock_context.window)) + self.assertTrue('is not a string' in sys.stdout.getvalue()) + + @staticmethod + def setting_value_gopath_data(): + return ( + ( + 'basic_shell', + '/bin/bash', + { + 'PATH': '/bin', + 'GOPATH': os.path.expanduser('~'), + }, + None, + None, + {}, + 'GOPATH', + (os.path.expanduser('~'), '/bin/bash'), + ), + ( + 'basic_shell_2', + '/bin/bash', + { + 'PATH': '/bin' + }, + None, + None, + {}, + 'PATH', + ('/bin', '/bin/bash'), + ), + ( + 'basic_view_settings', + '/bin/bash', + { + 'PATH': '/bin', + 'GOPATH': os.path.expanduser('~'), + }, + {'GOPATH': '/usr/bin'}, + None, + {}, + 'GOPATH', + ('/usr/bin', 'project file'), + ), + ( + 'basic_window_settings', + '/bin/bash', + { + 'PATH': '/bin', + 'GOPATH': os.path.expanduser('~'), + }, + None, + {'GOPATH': '/usr/bin'}, + {}, + 'GOPATH', + ('/usr/bin', 'project file'), + ), + ( + 'basic_sublime_settings', + '/bin/bash', + { + 'PATH': '/bin', + 'GOPATH': os.path.expanduser('~'), + }, + {}, + {}, + {'GOPATH': '/usr/local/bin'}, + 'GOPATH', + ('/usr/local/bin', 'golang.sublime-settings'), + ), + ( + 'os_view_settings', + '/bin/bash', + { + 'PATH': '/bin', + 'GOPATH': os.path.expanduser('~'), + }, + { + 'osx': {'GOPATH': '/usr/bin'}, + 'windows': {'GOPATH': '/usr/bin'}, + 'linux': {'GOPATH': '/usr/bin'}, + }, + {}, + {}, + 'GOPATH', + ('/usr/bin', 'project file (os-specific)'), + ), + ( + 'os_window_settings', + '/bin/bash', + { + 'PATH': '/bin', + 'GOPATH': os.path.expanduser('~'), + }, + None, + { + 'osx': {'GOPATH': '/usr/bin'}, + 'windows': {'GOPATH': '/usr/bin'}, + 'linux': {'GOPATH': '/usr/bin'}, + }, + {}, + 'GOPATH', + ('/usr/bin', 'project file (os-specific)'), + ), + ( + 'os_sublime_settings', + '/bin/bash', + { + 'PATH': '/bin', + 'GOPATH': os.path.expanduser('~'), + }, + { + 'GOPATH': '/foo/bar' + }, + {}, + { + 'osx': {'GOPATH': '/usr/local/bin'}, + 'windows': {'GOPATH': '/usr/local/bin'}, + 'linux': {'GOPATH': '/usr/local/bin'}, + }, + 'GOPATH', + ('/usr/local/bin', 'golang.sublime-settings (os-specific)'), + ), + ( + 'os_sublime_settings_wrong_type', + '/bin/bash', + { + 'PATH': '/bin', + 'GOPATH': os.path.expanduser('~'), + }, + {}, + {}, + { + 'osx': 1, + 'windows': 1, + 'linux': 1, + }, + 'GOPATH', + (os.path.expanduser('~'), '/bin/bash'), + ), + ) + + @data('setting_value_gopath_data', True) + def setting_value_gopath(self, shell, env, view_settings, window_settings, sublime_settings, setting, result): + + with GolangConfigMock(shell, env, view_settings, window_settings, sublime_settings) as mock_context: + self.assertEquals(result, golangconfig.setting_value(setting, mock_context.view, mock_context.window)) + self.assertEqual('', sys.stdout.getvalue()) + + def test_setting_value_bytes_name(self): + shell = '/bin/bash' + env = { + 'GOPATH': os.path.expanduser('~') + } + with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context: + def do_test(): + golangconfig.setting_value(b'GOPATH', mock_context.view, mock_context.window) + self.assertRaises(TypeError, do_test) + + def test_setting_value_custom_type(self): + shell = '/bin/bash' + env = { + 'GOPATH': os.path.expanduser('~') + } + with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context: + def do_test(): + golangconfig.setting_value(CustomString('GOPATH'), mock_context.view, mock_context.window) + self.assertRaises(TypeError, do_test) + + def test_setting_value_incorrect_view_type(self): + shell = '/bin/bash' + env = { + 'GOPATH': os.path.expanduser('~') + } + with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context: + def do_test(): + golangconfig.setting_value('GOPATH', True, mock_context.window) + self.assertRaises(TypeError, do_test) + + def test_setting_value_incorrect_window_type(self): + shell = '/bin/bash' + env = { + 'GOPATH': os.path.expanduser('~') + } + with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context: + def do_test(): + golangconfig.setting_value('GOPATH', mock_context.view, True) + self.assertRaises(TypeError, do_test) + + def test_setting_value_gopath_not_existing(self): + shell = '/bin/bash' + env = { + 'GOPATH': os.path.join(os.path.expanduser('~'), 'hdjsahkjzhkjzhiashs7hdsuybyusbguycas') + } + with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context: + self.assertEquals( + (None, None), + golangconfig.setting_value('GOPATH', mock_context.view, mock_context.window) + ) + self.assertTrue('does not exist on the filesystem' in sys.stdout.getvalue()) + + def test_setting_value_gopath_not_string(self): + shell = '/bin/bash' + env = { + 'GOPATH': 1 + } + with GolangConfigMock(shell, env, None, None, {'debug': True}) as mock_context: + self.assertEquals( + (None, None), + golangconfig.setting_value('GOPATH', mock_context.view, mock_context.window) + ) + self.assertTrue('is not a string' in sys.stdout.getvalue()) + + def test_subprocess_info_goroot_executable_not_inside(self): + shell = '/bin/bash' + env = { + 'PATH': '{tempdir}bin:{tempdir}go/bin', + 'GOPATH': '{tempdir}workspace', + 'GOROOT': '{tempdir}go' + } + with GolangConfigMock(shell, env, None, None, {}) as mock_context: + mock_context.replace_tempdir_env() + mock_context.replace_tempdir_view_settings() + mock_context.replace_tempdir_window_settings() + + mock_context.make_executable_files(['bin/go', 'go/bin/go']) + mock_context.make_dirs(['workspace']) + + golangconfig.subprocess_info( + 'go', + ['GOPATH'], + optional_vars=['GOROOT'], + view=mock_context.view, + window=mock_context.window + ) + self.assertTrue('which is not inside of the GOROOT' in sys.stdout.getvalue())
diff --git a/dev/unittest_data.py b/dev/unittest_data.py new file mode 100644 index 0000000..90d4eb5 --- /dev/null +++ b/dev/unittest_data.py
@@ -0,0 +1,55 @@ +# This file is Copyright 2015, Will Bond <will@wbond.net>, licensed to Google +# under the terms of the Google Inbound Service Agreement, signed August 15 2015 + + +def data(provider_method, first_param_name_suffix=False): + """ + A method decorator for unittest.TestCase classes that configured a + static method to be used to provide multiple sets of test data to a single + test + + :param provider_method: + The name of the staticmethod of the class to use as the data provider + + :param first_param_name_suffix: + If the first parameter for each set should be appended to the method + name to generate the name of the test. Otherwise integers are used. + + :return: + The decorated function + """ + + def test_func_decorator(test_func): + test_func._provider_method = provider_method + test_func._provider_name_suffix = first_param_name_suffix + return test_func + return test_func_decorator + + +def data_class(cls): + """ + A class decorator that works with the @provider decorator to generate test + method from a data provider + """ + + def generate_test_func(name, original_function, num, params): + if original_function._provider_name_suffix: + data_name = params[0] + params = params[1:] + else: + data_name = num + expanded_name = 'test_%s_%s' % (name, data_name) + # We used expanded variable names here since this line is present in + # backtraces that are generated from test failures. + generated_test_function = lambda self: original_function(self, *params) + setattr(cls, expanded_name, generated_test_function) + + for name in dir(cls): + func = getattr(cls, name) + if hasattr(func, '_provider_method'): + num = 1 + for params in getattr(cls, func._provider_method)(): + generate_test_func(name, func, num, params) + num += 1 + + return cls
diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..015d74e --- /dev/null +++ b/docs/changelog.md
@@ -0,0 +1,5 @@ +# golangconfig Changelog + +## 1.0.0 + + - Initial release
diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..66fd381 --- /dev/null +++ b/docs/design.md
@@ -0,0 +1,30 @@ +# golangconfig Design + +The `golangconfig` Sublime Text dependency was designed based on the following +ideas: + + - Settings should be supported coming from the following sources, in order: + - Sublime Text project files under the `golang` key + - Any `golang.sublime-settings` files + - The user's shell environment, as defined by invoking their login shell + - The project and global Sublime Text settings will also allow for placing + settings in a platform-specific sub-dictionary to allow users to easily + work across different operating systems. The keys for these are used by + other ST packages to allow for platform-specific functionality: + - "osx" + - "windows" + - "linux" + - Platform-specific settings are always higher priority than + non-platform-specific, no matter what source they are pulled from + - Setting names that are core to Go configuration preserve the uppercase style + of environment variables. Thus the settings are named `GOPATH`, `GOROOT`, + `PATH`, etc. + + + - When returning results, the value requested is returned along with a + user-friendly source description that can be used when displaying + configuration details to the user + - The API eschews duck-typing in an attempt to prevent various edge-case bugs. + This is largely due to the weak-typing issues of strings in Python 2 where + byte strings and unicode strings are often mixed, only to cause exceptions + at runtime on user machines where errors are harder to capture.
diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..bcf5e0c --- /dev/null +++ b/docs/development.md
@@ -0,0 +1,43 @@ +# golangconfig Development + +## Setup + + - Install [Package Coverage](https://packagecontrol.io/packages/Package%20Coverage) + to run tests + - Install the [shellenv](https://github.com/codexns/shellenv) dependency by + executing `git clone --branch 1.4.1 https://github.com/codexns/shellenv` + inside of your `Packages/` folder + - Install this dependency by + executing `git clone https://go.googlesource.com/sublime-config golangconfig` + inside of your `Packages/` folder + - Use the Package Control command "Install Local Dependency" to install + `shellenv` and then `golangconfig` so they are available to the Python plugin + environment + +## General Notes + + - All code must pass the checks of the Sublime Text package + [Python Flake8 Lint](https://packagecontrol.io/packages/Python%20Flake8%20Lint). + The `python_interpreter` setting should be set to `internal`. + - Tests and coverage measurement must be run in the UI thread since the package + utilizes the `sublime` API, which is not thread safe on ST2 + - Sublime Text 2 and 3 must be supported, on Windows, OS X and Linux + - In public-facing functions, types should be strictly checked to help reduce + edge-case bugs + - All functions must include a full docstring with parameter and return types + and a list of exceptions raised + - All code should use a consistent Python header + +```python +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function +``` + + - Markdown-based API documentation can be automatically copied from the source + code by executing `dev/api_docs.py` with a Python installation containing + the `CommonMark` package + +```bash +pip install CommonMark +python dev/api_docs.py +```
diff --git a/docs/package_developer.md b/docs/package_developer.md new file mode 100644 index 0000000..faef4bb --- /dev/null +++ b/docs/package_developer.md
@@ -0,0 +1,405 @@ +# golangconfig Package Developer Documentation + +`golangconfig` is an API for package developers to obtain configuration +information about a user's Go environment. It is distributed as a Package +Control dependency, and thus automatically installed when a package requiring +it is installed by an end-user. + + - [Overview](#overview) + - [Example](#example) + - [API Documentation](#api-documentation) + +## Overview + +### subprocess_info() + +`subprocess_info()` is the primary function that will be used in packages. +It accepts five parameters. The first two are positional: + + 1. the name of the requested executable, e.g. "go", "gofmt", "godoc" + 2. a list of required environment variables + +The three remaining parameters are keyword arguments: + + - optional_vars: a list of vars that will be pulled from project or Sublime + Text settings and used as environment variables + - view: a `sublime.View` object, if available + - window: a `sublime.Window` object, if available + +The function returns a two-element tuple containing the path to the requested +executable and a `dict` to pass via the `env=` arg of `subprocess.Popen()`. + +The `sublime.View` and/or `sublime.Window` objects are used to obtain +project-specific settings. These objects are available via attributes of the +`sublime_plugin.WindowCommand` and `sublime_plugin.TextCommand` classes. + +The `golangconfig` package interacts with Sublime Text's settings API, which +means that all calls must occur within the UI thread for compatiblity with +Sublime Text 2. + +### setting_value() + +The function `setting_value()` is intended for use when fetching environment +variables for non-subprocess usage, or for getting settings that are not +environment variables. Obtaining the value of an individual environment variable +may be useful when printing debug information, or using the value in an +interactive manner with the user. + +The function accepts three parameters. The first is position: + + 1. a unicode string of the name of the setting or environment variable + +The two other parameters are keyword arguments: + + - view: a `sublime.View` object, if available + - window: a `sublime.Window` object, if available + +The function returns a two-element tuple containing the value of the setting +requested, and a unicode string describing the source of the setting. + +If no value was found for the setting, the tuple `(None, None)` will be +returned. + +If a value is found for the setting, the second element of the tuple will +contain one of the following unicode strings: + + - "project file" + - "project file (os-specific)" + - "golang.sublime-settings" + - "golang.sublime-settings (os-specific)" + - a unicode string of the path to the user's login shell + +This value is intended for display to the user for help in debugging. + +### Errors + +If the executable can not be found, a `golangconfig.ExecutableError()` will be +raised. It has two attributes: `.name` which is the name of the executable that +could not be found, and `.dirs` which is a list of the dirs searched by first +looking at the `PATH` from the Sublime Text settings, and then looking at the +shell `PATH` value. + +If one of the required environment variables is not set, an +`golangconfig.EnvVarError()` will be raised. It has one attribute: `.missing` +which is a list of all required environment variables that could not be +found in the Sublime Text settings, or the shell environment. + +### Requiring the Dependency + +When developing a package to utilize `golangconfig`, Package Control needs to be +told to ensure that `golangconfig` is installed. To accomplish this, a file +named `dependencies.json` needs to be placed in the root of the package. The +file should contain the following specification: + +```json +{ + "*": { + "*": [ + "shellenv", + "golangconfig" + ] + } +} +``` + +This specification indicates that for all operating systems (the outer `*`) and +all versions of Sublime Text (the nested `*`), the dependencies named `shellenv` +and `golangconfig` are required. + +## Example + +The following snippet of Python show basic usage of `golangconfig` from within +command classes derived from `sublime_plugin.WindowCommand` and +`sublime_plugin.TextCommand`. + +```python +# coding: utf-8 +from __future__ import unicode_literals + +import sublime +import sublime_plugin + +import golangconfig + + +class MyWindowCommand(sublime_plugin.WindowCommand): + def run(self): + try: + go_executable_path, env_dict = golangconfig.subprocess_info( + 'go', + ['GOPATH'], + [ + 'GOROOT', + 'GOROOT_FINAL', + 'GOBIN', + 'GOOS', + 'GOARCH', + 'GORACE', + 'GOARM', + 'GO386', + 'GOHOSTOS', + 'GOHOSTARCH', + ], + window=self.window + ) + + # Launch thread to execute subprocess.Popen() ... + + except (golangconfig.ExecutableError) as e: + error_message = ''' + My Package + + The %s executable could not be found. Please ensure it is + installed and available via your PATH. + + Would you like to view documentation for setting the PATH? + ''' + + prompt = error_message % e.name + + if sublime.ok_cancel_dialog(prompt, 'Open Documentation'): + self.window.run_command( + 'open_url', + {'url': 'http://example.com/documentation'} + ) + + except (golangconfig.EnvVarError) as e: + error_message = ''' + My Package + + The setting%s %s could not be found in your Sublime Text + settings or your shell environment. + + Would you like to view the configuration documentation? + ''' + + plural = 's' if len(e.missing) > 1 else '' + setting_names = ', '.join(e.missing) + prompt = error_message % (plural, setting_names) + + if sublime.ok_cancel_dialog(prompt, 'Open Documentation'): + self.window.run_command( + 'open_url', + {'url': 'http://example.com/documentation'} + ) + + +class MyTextCommand(sublime_plugin.TextCommand): + def run(self): + # This example omits exception handling for brevity + gofmt_executable_path, env = golangconfig.subprocess_info( + 'gofmt', + ['GOPATH'], + # GOOS, GOARCH, GO386 and GOARM are omitted from optional_vars in + # this example with the intent they would be provided through user + # interaction. + [ + 'GOROOT', + 'GOROOT_FINAL', + 'GOBIN', + 'GORACE', + 'GOHOSTOS', + 'GOHOSTARCH', + ], + view=self.view + ) + + goos_setting = golangconfig.setting_value('GOOS', view=self.view) + goarch_setting = golangconfig.setting_value('GOARCH', view=self.view) + + # Use the sublime API to show the user OS and ARCH options, with their + # values from the settings selected by default + +``` + +Since the `golangconfig` functions must be called in the UI thread, commands +will normally look up any necessary information before firing off a thread to +perform a task in the background. + +## API Documentation + +The public API consists of the following functions: + + - [`subprocess_info()`](#subprocess_info-function) + - [`setting_value()`](#setting_value-function) + - [`executable_path()`](#executable_path-function) + - [`debug_enabled()`](#debug_enabled-function) + +### `subprocess_info()` function + +> ```python +> def subprocess_info(executable_name, required_vars, optional_vars=None, view=None, window=None): +> """ +> :param executable_name: +> A unicode string of the executable to locate, e.g. "go" or "gofmt" +> +> :param required_vars: +> A list of unicode strings of the environment variables that are +> required, e.g. "GOPATH". Obtains values from setting_value(). +> +> :param optional_vars: +> A list of unicode strings of the environment variables that are +> optional, but should be pulled from setting_value() if available - e.g. +> "GOOS", "GOARCH". Obtains values from setting_value(). +> +> :param view: +> A sublime.View object to use in finding project-specific settings. This +> should be passed whenever available. +> +> :param window: +> A sublime.Window object to use in finding project-specific settings. +> This will only work for Sublime Text 3, and should only be passed if +> no sublime.View object is available to pass via the view parameter. +> +> :raises: +> RuntimeError +> When the function is called from any thread but the UI thread +> TypeError +> When any of the parameters are of the wrong type +> golangconfig.ExecutableError +> When the executable requested could not be located. The .name +> attribute contains the name of the executable that could not be +> located. The .dirs attribute contains a list of unicode strings +> of the directories searched. +> golangconfig.EnvVarError +> When one or more required_vars are not available. The .missing +> attribute will be a list of the names of missing environment +> variables. +> +> :return: +> A two-element tuple. +> +> - [0] A unicode string (byte string for ST2) of the path to the executable +> - [1] A dict to pass to the env parameter of subprocess.Popen() +> """ +> ``` +> +> Gathers and formats information necessary to use subprocess.Popen() to +> run one of the go executables, with details pulled from setting_value() and +> executable_path(). +> +> Ensures that the executable path and env dictionary are properly encoded for +> Sublime Text 2, where byte strings are necessary. + +### `setting_value()` function + +> ```python +> def setting_value(setting_name, view=None, window=None): +> """ +> :param setting_name: +> A unicode string of the setting to retrieve +> +> :param view: +> A sublime.View object to use in finding project-specific settings. This +> should be passed whenever available. +> +> :param window: +> A sublime.Window object to use in finding project-specific settings. +> This will only work for Sublime Text 3, and should only be passed if +> no sublime.View object is available to pass via the view parameter. +> +> :raises: +> RuntimeError +> When the function is called from any thread but the UI thread +> TypeError +> When any of the parameters are of the wrong type +> +> :return: +> A two-element tuple. +> +> If no setting was found, the return value will be: +> +> - [0] None +> - [1] None +> +> If a setting was found, the return value will be: +> +> - [0] The setting value +> - [1] The source of the setting, a unicode string: +> - "project file (os-specific)" +> - "golang.sublime-settings (os-specific)" +> - "project file" +> - "golang.sublime-settings" +> - A unicode string of the path to the user's login shell +> +> The second element of the tuple is intended to be used in the display +> of debugging information to end users. +> """ +> ``` +> +> Returns the user's setting for a specific variable, such as GOPATH or +> GOROOT. Supports global and per-platform settings. Finds settings by +> looking in: +> +> 1. If a project is open, the project settings +> 2. The global golang.sublime-settings file +> 3. The user's environment variables, as defined by their login shell +> +> If the setting is a known name, e.g. GOPATH or GOROOT, the value will be +> checked to ensure the path exists. + +### `executable_path()` function + +> ```python +> def executable_path(executable_name, view=None, window=None): +> """ +> :param name: +> The name of the binary to find - a unicode string of "go", "gofmt" or +> "godoc" +> +> :param view: +> A sublime.View object to use in finding project-specific settings. This +> should be passed whenever available. +> +> :param window: +> A sublime.Window object to use in finding project-specific settings. +> This will only work for Sublime Text 3, and should only be passed if +> no sublime.View object is available to pass via the view parameter. +> +> :raises: +> RuntimeError +> When the function is called from any thread but the UI thread +> TypeError +> When any of the parameters are of the wrong type +> +> :return: +> A 2-element tuple. +> +> If the executable was not found, the return value will be: +> +> - [0] None +> - [1] None +> +> If the exeutable was found, the return value will be: +> +> - [0] A unicode string of the full path to the executable +> - [1] A unicode string of the source of the PATH value +> - "project file (os-specific)" +> - "golang.sublime-settings (os-specific)" +> - "project file" +> - "golang.sublime-settings" +> - A unicode string of the path to the user's login shell +> +> The second element of the tuple is intended to be used in the display +> of debugging information to end users. +> """ +> ``` +> +> Uses the user's Sublime Text settings and then PATH environment variable +> as set by their login shell to find a go executable + +### `debug_enabled()` function + +> ```python +> def debug_enabled(): +> """ +> :raises: +> RuntimeError +> When the function is called from any thread but the UI thread +> +> :return: +> A boolean - if debug is enabled +> """ +> ``` +> +> Checks to see if the "debug" setting is true
diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..74ee043 --- /dev/null +++ b/docs/readme.md
@@ -0,0 +1,14 @@ +# golangconfig Documentation + +The documentation for the package is split into two audiences: + + - [User documentation](user.md) describing how to configure Sublime Text + to properly work with your Go environment + - [Package developer documentation](package_developer.md) containing API + documentation and instructions on how to require `golangconfig` for your + package + +Other documentation: + + - [Design](design.md) + - [Development](development.md)
diff --git a/docs/user.md b/docs/user.md new file mode 100644 index 0000000..973d04f --- /dev/null +++ b/docs/user.md
@@ -0,0 +1,138 @@ +# golangconfig User Documentation + +The `golangconfig` package is a reusable library that Go-related Sublime Text +packages can use to obtain information about your Go environment. + +This documentation details how you can set OS-specific, per-project and global +Sublime Text configuration for all packages that utilize `golangconfig`. + + - [Environment Autodetection](#environment-autodetection) + - [Overriding the Environment](#overriding-the-environment) + - [Global Sublime Text Settings](#global-sublime-text-settings) + - [OS-Specific Settings](#os-specific-settings) + - [Project-Specific Settings](#project-specific-settings) + +## Environment Autodetection + +By default `golangconfig` tries to detect all of your Go configuration by +invoking your login shell. It will pull in your `PATH`, `GOPATH`, and any other +environment variables you have set. + +## Overriding the Environment + +Generally, autodetecting the shell environment is sufficient for most users +with a homogenous Go environment. If your Go configuration is more complex, +Sublime Text settings may be used to handle it, via: + + - [Global Sublime Text Settings](#global-sublime-text-settings) + - [OS-Specific Settings](#os-specific-settings) + - [Project-Specific Settings](#project-specific-settings) + +Settings are loading using the following precedence, from most-to-least +specific: + + - OS-specific project settings + - OS-specific global Sublime Text settings + - Project settings + - Global Sublime Text settings + - Shell environment + +### Global Sublime Text Settings + +To set variables for use in Sublime Text windows, you will want to edit your +`golang.sublime-settings` file. This can be accessed via the menu: + + 1. Preferences + 2. Package Settings + 3. Golang Config + 3. Settings - User + +Settings are placed in a json structure. Common settings include: + + - `PATH` - a list of directories to search for executables within. On Windows + these are separated by `;`. OS X and Linux use `:` as a directory separator. + - `GOPATH` - a string of the path to the root of your Go environment + +Other Go settings may, or may not, be supported by the packages using these +settings. Examples include: `GOOS`, `GOARCH`, `GOROOT`. + +```json +{ + "PATH": "/Users/jsmith/go/bin", + "GOPATH": "/Users/jsmith/go" +} +``` + +### OS-Specific Settings + +For users that are working on different operating systems, it may be necessary +to segement settings per OS. All settings may be nested under a key of one of +the following strings: + + - "osx" + - "windows" + - "linux" + +```json +{ + "osx": { + "PATH": "/Users/jsmith/go/bin", + "GOPATH": "/Users/jsmith/go" + }, + "windows": { + "PATH": "C:\\Users\\jsmith\\go\\bin", + "GOPATH": "C:\\Users\\jsmith\\go" + }, + "linux": { + "PATH": "/home/jsmith/go/bin", + "GOPATH": "/home/jsmith/go" + }, +} +``` + +### Project-Specific Settings + +When working on Go projects that use different environments, it may be +necessary to define settings in a +[Sublime Text project](http://docs.sublimetext.info/en/latest/file_management/file_management.html#projects) +file. The *Project* menu in Sublime Text provides the interface to create and +edit project files. + +Within projects, all Go settings are placed under the `"settings"` key and then +further under a subkey named `"golang"`. + +```json +{ + "folders": { + "/Users/jsmith/projects/myproj" + }, + "settings": { + "golang": { + "PATH": "/Users/jsmith/projects/myproj/env/bin", + "GOPATH": "/Users/jsmith/projects/myproj/env" + } + } +} +``` + +Project-specific settings may also utilize the OS-specific settings feature. + +```json +{ + "folders": { + "/Users/jsmith/projects/myproj" + }, + "settings": { + "golang": { + "osx": { + "PATH": "/Users/jsmith/projects/myproj/env/bin", + "GOPATH": "/Users/jsmith/projects/myproj/env" + }, + "linux": { + "PATH": "/home/jsmith/projects/myproj/env/bin", + "GOPATH": "/home/jsmith/projects/myproj/env" + } + } + } +} +```
diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0c9caf7 --- /dev/null +++ b/readme.md
@@ -0,0 +1,21 @@ +# golangconfig + +`golangconfig` is a Sublime Text dependency designed to be a common API for +configuration of Go environment variables. It is intended to be used by any and +all Go-related Sublime Text packages in an effort to help reduce duplication +of user configuration. + +The documentation for the package is split into two audiences: + + - [User documentation](docs/user.md) describing how to configure Sublime Text + to properly work with your Go environment + - [Package developer documentation](docs/package_developer.md) containing API + documentation and instructions on how to require `golangconfig` for your + package + +Other documentation: + + - [Changelog](docs/changelog.md) + - [License](LICENSE) + - [Design](docs/design.md) + - [Development](docs/development.md)