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)