Repository: mkdocs/mkdocs

Issue:
Paths are relative to you, not the config file
You can do `mkdocs build --config-file=path/to/mkdocs.yml` but any configuration values in that file are relative to your current working directory.

This seems wrong to me, but it's only something I've ran into when trying to automate MkDocs builds (for integration testing). I'm not sure if we want to change this, or what people really expect. The common use case is that you are in the same directory as the config file anyway, so that doen't matter.

Something like this should be changed or at least decided for a 1.0 release.

Thoughts?


Discussion:
waylan: Awesome! I'll have to take a closer look when I have some time.
djeebus: Just found an issue: plugins aren't getting normalized properly. Should have it fixed soon too.
djeebus: All ready now!
waylan: The py33 tests are being handled in #1453. Guess we can move on with this issue by updating the docs.

Patch:
diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md
index e5a2abaf..9ae74fb1 100644
--- a/docs/about/release-notes.md
+++ b/docs/about/release-notes.md
@@ -25,6 +25,25 @@ The current and past members of the MkDocs team.
 
 ### Major Additions to Development Version
 
+#### Path Based Settings are Relative to Configuration File (#543)
+
+Previously any relative paths in the various configuration options were
+resolved relative to the current working directory. They are now resolved
+relative to the configuration file. As the documentation has always encouraged
+running the various MkDocs commands from the directory that contains the
+configuration file (project root), this change will not affect most users.
+However, it will make it much easier to implement automated builds or otherwise
+run commands from a location other than the project root.
+
+Simply use the `-f/--config-file` option and point it at the configuration file:
+
+```sh
+mkdocs build --config-file /path/to/my/config/file.yml
+```
+
+As previously, if no file is specified, MkDocs looks for a file named
+`mkdocs.yml` in the current working directory.
+
 #### Refactor Search Plugin
 
 The search plugin has been completely refactored to include support for the
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index 333b7e67..8cac62f9 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -215,9 +215,10 @@ If a set of key/value pairs, the following nested keys can be defined:
 
     #### custom_dir:
 
-    A directory to custom a theme. This can either be a relative directory, in
-    which case it is resolved relative to the directory containing your
-    configuration file, or it can be an absolute directory path.
+    A directory containing a custom theme. This can either be a relative
+    directory, in which case it is resolved relative to the directory containing
+    your configuration file, or it can be an absolute directory path from the
+    root of your local file system.
 
     See [styling your docs][theme_dir] for details if you would like to tweak an
     existing theme.
@@ -240,19 +241,19 @@ If a set of key/value pairs, the following nested keys can be defined:
 
 ### docs_dir
 
-Lets you set the directory containing the documentation source markdown files.
-This can either be a relative directory, in which case it is resolved relative
-to the directory containing your configuration file, or it can be an absolute
-directory path from the root of your local file system.
+The directory containing the documentation source markdown files. This can
+either be a relative directory, in which case it is resolved relative to the
+directory containing your configuration file, or it can be an absolute directory
+path from the root of your local file system.
 
 **default**: `'docs'`
 
 ### site_dir
 
-Lets you set the directory where the output HTML and other files are created.
-This can either be a relative directory, in which case it is resolved relative
-to the directory containing your configuration file, or it can be an absolute
-directory path from the root of your local file system.
+The directory where the output HTML and other files are created. This can either
+be a relative directory, in which case it is resolved relative to the directory
+containing your configuration file, or it can be an absolute directory path from
+the root of your local file system.
 
 **default**: `'site'`
 
diff --git a/docs/user-guide/custom-themes.md b/docs/user-guide/custom-themes.md
index 2f0021dd..18f72b28 100644
--- a/docs/user-guide/custom-themes.md
+++ b/docs/user-guide/custom-themes.md
@@ -20,41 +20,48 @@ and their usage.
 ## Creating a custom theme
 
 The bare minimum required for a custom theme is a `main.html` [Jinja2 template]
-file. This should be placed in a directory which will be the `custom_dir` and it
-should be created next to the `mkdocs.yml` configuration file. Within
-`mkdocs.yml`, specify the theme `custom_dir` option and set it to the name of
-the directory containing `main.html`. For example, given this example project
-layout:
-
-    mkdocs.yml
-    docs/
-        index.md
-        about.md
-    custom_theme/
-        main.html
-        ...
-
-You would include the following settings in `mkdocs.yml` to use the custom theme
+file which is placed in a directory that is *not* a child of the [docs_dir].
+Within `mkdocs.yml`, set the theme.[custom_dir] option to the path of the
+directory containing `main.html`. The path should be relative to the
+configuration file. For example, given this example project layout:
+
+```no-highlight
+mkdocs.yml
+docs/
+    index.md
+    about.md
+custom_theme/
+    main.html
+    ...
+```
+
+... you would include the following settings in `mkdocs.yml` to use the custom theme
 directory:
 
-    theme:
-        name: null
-        custom_dir: 'custom_theme'
+```yaml
+theme:
+    name: null
+    custom_dir: 'custom_theme/'
+```
 
 !!! Note
 
-    Generally, when building your own custom theme, the theme `name`
-    configuration setting would be set to `null`. However, if used in
-    combination with the `custom_dir` configuration value a custom theme can be
-    used to replace only specific parts of a built-in theme. For example, with
-    the above layout and if you set `name: "mkdocs"` then the `main.html` file
-    in the `custom_dir` would replace that in the theme but otherwise the
-    `mkdocs` theme would remain the same. This is useful if you want to make
+    Generally, when building your own custom theme, the theme.[name]
+    configuration setting would be set to `null`. However, if the
+    theme.[custom_dir] configuration value is used in combination with an
+    existing theme, the theme.[custom_dir] can be used to replace only specific
+    parts of a built-in theme. For example, with the above layout and if you set
+    `name: "mkdocs"` then the `main.html` file in the theme.[custom_dir] would
+    replace the file of the same name in the `mkdocs` theme but otherwise the
+    `mkdocs` theme would remain unchanged. This is useful if you want to make
     small adjustments to an existing theme.
 
     For more specific information, see [styling your docs].
 
 [styling your docs]: ./styling-your-docs.md#using-the-theme-custom_dir
+[custom_dir]: ./configuration.md#custom_dir
+[name]: ./configuration.md#name
+[docs_dir]:./configuration.md#docs_dir
 
 ## Basic theme
 
diff --git a/docs/user-guide/styling-your-docs.md b/docs/user-guide/styling-your-docs.md
index 4837e8f6..c97abf99 100644
--- a/docs/user-guide/styling-your-docs.md
+++ b/docs/user-guide/styling-your-docs.md
@@ -138,7 +138,7 @@ And then point your `mkdocs.yml` configuration file at the new directory:
 ```yaml
 theme:
     name: mkdocs
-    custom_dir: custom_theme
+    custom_dir: custom_theme/
 ```
 
 To override the 404 error page ("file not found"), add a new template file named
diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py
index afcec0a9..60f9b587 100644
--- a/mkdocs/config/base.py
+++ b/mkdocs/config/base.py
@@ -21,13 +21,14 @@ class Config(utils.UserDict):
     for running validation on the structure and contents.
     """
 
-    def __init__(self, schema):
+    def __init__(self, schema, config_file_path=None):
         """
         The schema is a Python dict which maps the config name to a validator.
         """
 
         self._schema = schema
         self._schema_keys = set(dict(schema).keys())
+        self.config_file_path = config_file_path
         self.data = {}
 
         self.user_configs = []
@@ -172,7 +173,7 @@ def load_config(config_file=None, **kwargs):
 
     # Initialise the config with the default schema .
     from mkdocs import config
-    cfg = Config(schema=config.DEFAULT_SCHEMA)
+    cfg = Config(schema=config.DEFAULT_SCHEMA, config_file_path=options['config_file_path'])
     # First load the config file
     cfg.load_file(config_file)
     # Then load the options to overwrite anything in the config.
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index 9a7eb0b6..30da2a2a 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -293,6 +293,24 @@ class FilesystemObject(Type):
         super(FilesystemObject, self).__init__(type_=utils.string_types, **kwargs)
         self.exists = exists
 
+    def pre_validation(self, config, key_name):
+        value = config[key_name]
+
+        if not value:
+            return
+
+        if os.path.isabs(value):
+            return
+
+        if config.config_file_path is None:
+            # Unable to determine absolute path of the config file; fall back
+            # to trusting the relative path
+            return
+
+        config_dir = os.path.dirname(config.config_file_path)
+        value = os.path.join(config_dir, value)
+        config[key_name] = value
+
     def run_validation(self, value):
         value = super(FilesystemObject, self).run_validation(value)
         if self.exists and not self.existence_test(value):
@@ -311,9 +329,11 @@ class Dir(FilesystemObject):
     name = 'directory'
 
     def post_validation(self, config, key_name):
+        if config.config_file_path is None:
+            return
 
         # Validate that the dir is not the parent dir of the config file.
-        if os.path.dirname(config['config_file_path']) == config[key_name]:
+        if os.path.dirname(config.config_file_path) == config[key_name]:
             raise ValidationError(
                 ("The '{0}' should not be the parent directory of the config "
                  "file. Use a child directory instead so that the config file "
@@ -430,7 +450,12 @@ class Theme(BaseConfigOption):
 
         # Ensure custom_dir is an absolute path
         if 'custom_dir' in theme_config and not os.path.isabs(theme_config['custom_dir']):
-            theme_config['custom_dir'] = os.path.abspath(theme_config['custom_dir'])
+            config_dir = os.path.dirname(config.config_file_path)
+            theme_config['custom_dir'] = os.path.join(config_dir, theme_config['custom_dir'])
+
+        if 'custom_dir' in theme_config and not os.path.isdir(theme_config['custom_dir']):
+            raise ValidationError("The path set in {name}.custom_dir ('{path}') does not exist.".
+                                  format(path=theme_config['custom_dir'], name=self.name))
 
         config[key_name] = theme.Theme(**theme_config)
 
@@ -621,6 +646,10 @@ class Plugins(OptionallyRequired):
     def __init__(self, **kwargs):
         super(Plugins, self).__init__(**kwargs)
         self.installed_plugins = plugins.get_plugins()
+        self.config_file_path = None
+
+    def pre_validation(self, config, key_name):
+        self.config_file_path = config.config_file_path
 
     def run_validation(self, value):
         if not isinstance(value, (list, tuple)):
@@ -635,11 +664,15 @@ class Plugins(OptionallyRequired):
                 if not isinstance(cfg, dict):
                     raise ValidationError('Invalid config options for '
                                           'the "{0}" plugin.'.format(name))
-                plgins[name] = self.load_plugin(name, cfg)
-            elif isinstance(item, utils.string_types):
-                plgins[item] = self.load_plugin(item, {})
+                item = name
             else:
+                cfg = {}
+
+            if not isinstance(item, utils.string_types):
                 raise ValidationError('Invalid Plugins configuration')
+
+            plgins[item] = self.load_plugin(item, cfg)
+
         return plgins
 
     def load_plugin(self, name, config):
@@ -654,7 +687,7 @@ class Plugins(OptionallyRequired):
                 plugins.BasePlugin.__name__))
 
         plugin = Plugin()
-        errors, warnings = plugin.load_config(config)
+        errors, warnings = plugin.load_config(config, self.config_file_path)
         self.warnings.extend(warnings)
         errors_message = '\n'.join(
             "Plugin value: '{}'. Error: {}".format(x, y)
diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py
index 69d99ed8..de4e837d 100644
--- a/mkdocs/plugins.py
+++ b/mkdocs/plugins.py
@@ -42,10 +42,10 @@ class BasePlugin(object):
     config_scheme = ()
     config = {}
 
-    def load_config(self, options):
+    def load_config(self, options, config_file_path=None):
         """ Load config from a dict of options. Returns a tuple of (errors, warnings)."""
 
-        self.config = Config(schema=self.config_scheme)
+        self.config = Config(schema=self.config_scheme, config_file_path=config_file_path)
         self.config.load_dict(options)
 
         return self.config.validate()
diff --git a/tox.ini b/tox.ini
index 52d0d7e0..bee768b2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 envlist =
-    py{27,34,35,36}-{unittests,integration,min-req},
+    py{27,34,35,36,py,py3}-{unittests,integration,min-req},
     flake8, markdown-lint, linkchecker, jshint, csslint
 
 [testenv]
@@ -9,6 +9,7 @@ deps=
     py{27,34,35,36,py,py3}-{unittests,integration}: -rrequirements/project.txt
     py{27,34,35,36,py,py3}-min-req: -rrequirements/project-min.txt
     py{27,34,35,36,py,py3}-{unittests,min-req}: -rrequirements/test.txt
+    py{27,py}-{unittests,min-req}: backports.tempfile
 commands=
     {envpython} --version
     py{27,34,35,36,py,py3}-{unittests,min-req}: {envbindir}/nosetests --with-coverage --cover-package mkdocs mkdocs


Test patch:
diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py
index ac5f5116..32b86206 100644
--- a/mkdocs/tests/build_tests.py
+++ b/mkdocs/tests/build_tests.py
@@ -3,8 +3,6 @@
 
 from __future__ import unicode_literals
 import os
-import shutil
-import tempfile
 import unittest
 import mock
 import io
@@ -15,6 +13,11 @@ except ImportError:
     # In Py3 use builtin zip function
     pass
 
+try:
+    # py>=3.2
+    from tempfile import TemporaryDirectory
+except ImportError:
+    from backports.tempfile import TemporaryDirectory
 
 from mkdocs import nav
 from mkdocs.commands import build
@@ -319,9 +322,7 @@ class BuildTests(unittest.TestCase):
         self.assertEqual(page.content.strip(), '<p>foo</p>')
 
     def test_copying_media(self):
-        docs_dir = tempfile.mkdtemp()
-        site_dir = tempfile.mkdtemp()
-        try:
+        with TemporaryDirectory() as docs_dir, TemporaryDirectory() as site_dir:
             # Create a non-empty markdown file, image, html file, dot file and dot directory.
             f = open(os.path.join(docs_dir, 'index.md'), 'w')
             f.write(dedent("""
@@ -351,14 +352,9 @@ class BuildTests(unittest.TestCase):
             self.assertTrue(os.path.isfile(os.path.join(site_dir, 'example.html')))
             self.assertFalse(os.path.isfile(os.path.join(site_dir, '.hidden')))
             self.assertFalse(os.path.isfile(os.path.join(site_dir, '.git/hidden')))
-        finally:
-            shutil.rmtree(docs_dir)
-            shutil.rmtree(site_dir)
 
     def test_copy_theme_files(self):
-        docs_dir = tempfile.mkdtemp()
-        site_dir = tempfile.mkdtemp()
-        try:
+        with TemporaryDirectory() as docs_dir, TemporaryDirectory() as site_dir:
             # Create a non-empty markdown file.
             f = open(os.path.join(docs_dir, 'index.md'), 'w')
             f.write(dedent("""
@@ -383,9 +379,6 @@ class BuildTests(unittest.TestCase):
             self.assertFalse(os.path.isfile(os.path.join(site_dir, 'base.html')))
             self.assertFalse(os.path.isfile(os.path.join(site_dir, 'content.html')))
             self.assertFalse(os.path.isfile(os.path.join(site_dir, 'nav.html')))
-        finally:
-            shutil.rmtree(docs_dir)
-            shutil.rmtree(site_dir)
 
     def test_strict_mode_valid(self):
         pages = [
@@ -467,9 +460,7 @@ class BuildTests(unittest.TestCase):
         self.assertEqual(context['config']['extra']['a'], 1)
 
     def test_BOM(self):
-        docs_dir = tempfile.mkdtemp()
-        site_dir = tempfile.mkdtemp()
-        try:
+        with TemporaryDirectory() as docs_dir, TemporaryDirectory() as site_dir:
             # Create an UTF-8 Encoded file with BOM (as Micorsoft editors do). See #1186.
             f = io.open(os.path.join(docs_dir, 'index.md'), 'w', encoding='utf-8-sig')
             f.write('# An UTF-8 encoded file with a BOM')
@@ -490,7 +481,3 @@ class BuildTests(unittest.TestCase):
             self.assertTrue(
                 '<h1 id="an-utf-8-encoded-file-with-a-bom">An UTF-8 encoded file with a BOM</h1>' in output
             )
-
-        finally:
-            shutil.rmtree(docs_dir)
-            shutil.rmtree(site_dir)
diff --git a/mkdocs/tests/config/base_tests.py b/mkdocs/tests/config/base_tests.py
index ce56cd93..5118279f 100644
--- a/mkdocs/tests/config/base_tests.py
+++ b/mkdocs/tests/config/base_tests.py
@@ -3,6 +3,12 @@ import os
 import tempfile
 import unittest
 
+try:
+    # py>=3.2
+    from tempfile import TemporaryDirectory
+except ImportError:
+    from backports.tempfile import TemporaryDirectory
+
 from mkdocs import exceptions
 from mkdocs.config import base, defaults
 from mkdocs.config.config_options import BaseConfigOption
@@ -42,7 +48,9 @@ class ConfigBaseTests(unittest.TestCase):
         Allows users to specify a config other than the default `mkdocs.yml`.
         """
 
-        config_file = tempfile.NamedTemporaryFile('w', delete=False)
+        temp_dir = TemporaryDirectory()
+        config_file = open(os.path.join(temp_dir.name, 'mkdocs.yml'), 'w')
+        os.mkdir(os.path.join(temp_dir.name, 'docs'))
         try:
             config_file.write("site_name: MkDocs Test\n")
             config_file.flush()
@@ -64,7 +72,12 @@ class ConfigBaseTests(unittest.TestCase):
         `load_config` can accept an open file descriptor.
         """
 
-        config_file = tempfile.NamedTemporaryFile('r+', delete=False)
+        temp_dir = TemporaryDirectory()
+        temp_path = temp_dir.name
+        config_fname = os.path.join(temp_path, 'mkdocs.yml')
+
+        config_file = open(config_fname, 'w+')
+        os.mkdir(os.path.join(temp_path, 'docs'))
         try:
             config_file.write("site_name: MkDocs Test\n")
             config_file.flush()
@@ -75,7 +88,7 @@ class ConfigBaseTests(unittest.TestCase):
             # load_config will always close the file
             self.assertTrue(config_file.closed)
         finally:
-            os.remove(config_file.name)
+            temp_dir.cleanup()
 
     def test_load_from_closed_file(self):
         """
@@ -83,7 +96,10 @@ class ConfigBaseTests(unittest.TestCase):
         Ensure `load_config` reloads the closed file.
         """
 
-        config_file = tempfile.NamedTemporaryFile('w', delete=False)
+        temp_dir = TemporaryDirectory()
+        config_file = open(os.path.join(temp_dir.name, 'mkdocs.yml'), 'w')
+        os.mkdir(os.path.join(temp_dir.name, 'docs'))
+
         try:
             config_file.write("site_name: MkDocs Test\n")
             config_file.flush()
@@ -93,7 +109,7 @@ class ConfigBaseTests(unittest.TestCase):
             self.assertTrue(isinstance(cfg, base.Config))
             self.assertEqual(cfg['site_name'], 'MkDocs Test')
         finally:
-            os.remove(config_file.name)
+            temp_dir.cleanup()
 
     def test_load_from_deleted_file(self):
         """
@@ -234,3 +250,28 @@ class ConfigBaseTests(unittest.TestCase):
             ('invalid_option', 'run_validation warning'),
             ('invalid_option', 'post_validation warning'),
         ])
+
+    def test_load_from_file_with_relative_paths(self):
+        """
+        When explicitly setting a config file, paths should be relative to the
+        config file, not the working directory.
+        """
+
+        config_dir = TemporaryDirectory()
+        config_fname = os.path.join(config_dir.name, 'mkdocs.yml')
+        docs_dir = os.path.join(config_dir.name, 'src')
+        os.mkdir(docs_dir)
+
+        config_file = open(config_fname, 'w')
+
+        try:
+            config_file.write("docs_dir: src\nsite_name: MkDocs Test\n")
+            config_file.flush()
+            config_file.close()
+
+            cfg = base.load_config(config_file=config_file)
+            self.assertTrue(isinstance(cfg, base.Config))
+            self.assertEqual(cfg['site_name'], 'MkDocs Test')
+            self.assertEqual(cfg['docs_dir'], docs_dir)
+        finally:
+            config_dir.cleanup()
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
index 17c4141b..56b7a724 100644
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -6,6 +6,7 @@ import unittest
 import mkdocs
 from mkdocs import utils
 from mkdocs.config import config_options
+from mkdocs.config.base import Config
 
 
 class OptionallyRequiredTest(unittest.TestCase):
@@ -272,18 +273,21 @@ class DirTest(unittest.TestCase):
                           option.validate, [])
 
     def test_doc_dir_is_config_dir(self):
+        cfg = Config(
+            [('docs_dir', config_options.Dir())],
+            config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'),
+        )
 
         test_config = {
-            'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml'),
             'docs_dir': '.'
         }
 
-        docs_dir = config_options.Dir()
+        cfg.load_dict(test_config)
 
-        test_config['docs_dir'] = docs_dir.validate(test_config['docs_dir'])
+        fails, warns = cfg.validate()
 
-        self.assertRaises(config_options.ValidationError,
-                          docs_dir.post_validation, test_config, 'docs_dir')
+        self.assertEqual(len(fails), 1)
+        self.assertEqual(len(warns), 0)
 
 
 class SiteDirTest(unittest.TestCase):
@@ -293,12 +297,23 @@ class SiteDirTest(unittest.TestCase):
         site_dir = config_options.SiteDir()
         docs_dir = config_options.Dir()
 
-        config['config_file_path'] = os.path.join(os.path.abspath('..'), 'mkdocs.yml')
+        fname = os.path.join(os.path.abspath('..'), 'mkdocs.yml')
 
         config['docs_dir'] = docs_dir.validate(config['docs_dir'])
         config['site_dir'] = site_dir.validate(config['site_dir'])
-        site_dir.post_validation(config, 'site_dir')
-        return True  # No errors were raised
+
+        schema = [
+            ('site_dir', site_dir),
+            ('docs_dir', docs_dir),
+        ]
+        cfg = Config(schema, fname)
+        cfg.load_dict(config)
+        failed, warned = cfg.validate()
+
+        if failed:
+            raise config_options.ValidationError(failed)
+
+        return True
 
     def test_doc_dir_in_site_dir(self):
 
diff --git a/mkdocs/tests/config/config_tests.py b/mkdocs/tests/config/config_tests.py
index ebfdba48..d956bf3b 100644
--- a/mkdocs/tests/config/config_tests.py
+++ b/mkdocs/tests/config/config_tests.py
@@ -3,10 +3,16 @@
 
 from __future__ import unicode_literals
 import os
-import shutil
 import tempfile
 import unittest
 
+try:
+    # py>=3.2
+    from tempfile import TemporaryDirectory
+except ImportError:
+    from backports.tempfile import TemporaryDirectory
+
+
 import mkdocs
 from mkdocs import config
 from mkdocs import utils
@@ -81,8 +87,11 @@ class ConfigTests(unittest.TestCase):
         pages:
         - 'Introduction': 'index.md'
         """)
-        config_file = tempfile.NamedTemporaryFile('w', delete=False)
-        try:
+        with TemporaryDirectory() as temp_path:
+            os.mkdir(os.path.join(temp_path, 'docs'))
+            config_path = os.path.join(temp_path, 'mkdocs.yml')
+            config_file = open(config_path, 'w')
+
             config_file.write(ensure_utf(file_contents))
             config_file.flush()
             config_file.close()
@@ -90,93 +99,87 @@ class ConfigTests(unittest.TestCase):
             result = config.load_config(config_file=config_file.name)
             self.assertEqual(result['site_name'], expected_result['site_name'])
             self.assertEqual(result['pages'], expected_result['pages'])
-        finally:
-            os.remove(config_file.name)
 
     def test_theme(self):
-
-        mytheme = tempfile.mkdtemp()
-        custom = tempfile.mkdtemp()
-
-        configs = [
-            dict(),  # default theme
-            {"theme": "readthedocs"},  # builtin theme
-            {"theme_dir": mytheme},  # custom only
-            {"theme": "readthedocs", "theme_dir": custom},  # builtin and custom
-            {"theme": {'name': 'readthedocs'}},  # builtin as complex
-            {"theme": {'name': None, 'custom_dir': mytheme}},  # custom only as complex
-            {"theme": {'name': 'readthedocs', 'custom_dir': custom}},  # builtin and custom as complex
-            {  # user defined variables
-                'theme': {
-                    'name': 'mkdocs',
-                    'static_templates': ['foo.html'],
-                    'show_sidebar': False,
-                    'some_var': 'bar'
+        with TemporaryDirectory() as mytheme, TemporaryDirectory() as custom:
+            configs = [
+                dict(),  # default theme
+                {"theme": "readthedocs"},  # builtin theme
+                {"theme_dir": mytheme},  # custom only
+                {"theme": "readthedocs", "theme_dir": custom},  # builtin and custom
+                {"theme": {'name': 'readthedocs'}},  # builtin as complex
+                {"theme": {'name': None, 'custom_dir': mytheme}},  # custom only as complex
+                {"theme": {'name': 'readthedocs', 'custom_dir': custom}},  # builtin and custom as complex
+                {  # user defined variables
+                    'theme': {
+                        'name': 'mkdocs',
+                        'static_templates': ['foo.html'],
+                        'show_sidebar': False,
+                        'some_var': 'bar'
+                    }
                 }
-            }
-        ]
-
-        mkdocs_dir = os.path.abspath(os.path.dirname(mkdocs.__file__))
-        mkdocs_templates_dir = os.path.join(mkdocs_dir, 'templates')
-        theme_dir = os.path.abspath(os.path.join(mkdocs_dir, 'themes'))
-
-        results = (
-            {
-                'dirs': [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir],
-                'static_templates': ['404.html', 'sitemap.xml'],
-                'vars': {'include_search_page': False, 'search_index_only': False}
-            }, {
-                'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
-                'static_templates': ['404.html', 'sitemap.xml'],
-                'vars': {'include_search_page': True, 'search_index_only': False}
-            }, {
-                'dirs': [mytheme, mkdocs_templates_dir],
-                'static_templates': ['sitemap.xml'],
-                'vars': {}
-            }, {
-                'dirs': [custom, os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
-                'static_templates': ['404.html', 'sitemap.xml'],
-                'vars': {'include_search_page': True, 'search_index_only': False}
-            }, {
-                'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
-                'static_templates': ['404.html', 'sitemap.xml'],
-                'vars': {'include_search_page': True, 'search_index_only': False}
-            }, {
-                'dirs': [mytheme, mkdocs_templates_dir],
-                'static_templates': ['sitemap.xml'],
-                'vars': {}
-            }, {
-                'dirs': [custom, os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
-                'static_templates': ['404.html', 'sitemap.xml'],
-                'vars': {'include_search_page': True, 'search_index_only': False}
-            }, {
-                'dirs': [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir],
-                'static_templates': ['404.html', 'sitemap.xml', 'foo.html'],
-                'vars': {
-                    'show_sidebar': False,
-                    'some_var': 'bar',
-                    'include_search_page': False,
-                    'search_index_only': False
+            ]
+
+            mkdocs_dir = os.path.abspath(os.path.dirname(mkdocs.__file__))
+            mkdocs_templates_dir = os.path.join(mkdocs_dir, 'templates')
+            theme_dir = os.path.abspath(os.path.join(mkdocs_dir, 'themes'))
+
+            results = (
+                {
+                    'dirs': [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir],
+                    'static_templates': ['404.html', 'sitemap.xml'],
+                    'vars': {'include_search_page': False, 'search_index_only': False}
+                }, {
+                    'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
+                    'static_templates': ['404.html', 'sitemap.xml'],
+                    'vars': {'include_search_page': True, 'search_index_only': False}
+                }, {
+                    'dirs': [mytheme, mkdocs_templates_dir],
+                    'static_templates': ['sitemap.xml'],
+                    'vars': {}
+                }, {
+                    'dirs': [custom, os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
+                    'static_templates': ['404.html', 'sitemap.xml'],
+                    'vars': {'include_search_page': True, 'search_index_only': False}
+                }, {
+                    'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
+                    'static_templates': ['404.html', 'sitemap.xml'],
+                    'vars': {'include_search_page': True, 'search_index_only': False}
+                }, {
+                    'dirs': [mytheme, mkdocs_templates_dir],
+                    'static_templates': ['sitemap.xml'],
+                    'vars': {}
+                }, {
+                    'dirs': [custom, os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
+                    'static_templates': ['404.html', 'sitemap.xml'],
+                    'vars': {'include_search_page': True, 'search_index_only': False}
+                }, {
+                    'dirs': [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir],
+                    'static_templates': ['404.html', 'sitemap.xml', 'foo.html'],
+                    'vars': {
+                        'show_sidebar': False,
+                        'some_var': 'bar',
+                        'include_search_page': False,
+                        'search_index_only': False
+                    }
                 }
-            }
-        )
+            )
 
-        for config_contents, result in zip(configs, results):
+            for config_contents, result in zip(configs, results):
 
-            c = config.Config(schema=(
-                ('theme', config_options.Theme(default='mkdocs')),
-                ('theme_dir', config_options.ThemeDir(exists=True)),
-            ))
-            c.load_dict(config_contents)
-            errors, warnings = c.validate()
-            self.assertEqual(len(errors), 0)
-            self.assertEqual(c['theme'].dirs, result['dirs'])
-            self.assertEqual(c['theme'].static_templates, set(result['static_templates']))
-            self.assertEqual(dict([(k, c['theme'][k]) for k in iter(c['theme'])]), result['vars'])
+                c = config.Config(schema=(
+                    ('theme', config_options.Theme(default='mkdocs')),
+                    ('theme_dir', config_options.ThemeDir(exists=True)),
+                ))
+                c.load_dict(config_contents)
+                errors, warnings = c.validate()
+                self.assertEqual(len(errors), 0)
+                self.assertEqual(c['theme'].dirs, result['dirs'])
+                self.assertEqual(c['theme'].static_templates, set(result['static_templates']))
+                self.assertEqual(dict([(k, c['theme'][k]) for k in iter(c['theme'])]), result['vars'])
 
     def test_default_pages(self):
-        tmp_dir = tempfile.mkdtemp()
-        try:
+        with TemporaryDirectory() as tmp_dir:
             open(os.path.join(tmp_dir, 'index.md'), 'w').close()
             open(os.path.join(tmp_dir, 'about.md'), 'w').close()
             conf = config.Config(schema=config.DEFAULT_SCHEMA)
@@ -187,12 +190,9 @@ class ConfigTests(unittest.TestCase):
             })
             conf.validate()
             self.assertEqual(['index.md', 'about.md'], conf['pages'])
-        finally:
-            shutil.rmtree(tmp_dir)
 
     def test_default_pages_nested(self):
-        tmp_dir = tempfile.mkdtemp()
-        try:
+        with TemporaryDirectory() as tmp_dir:
             open(os.path.join(tmp_dir, 'index.md'), 'w').close()
             open(os.path.join(tmp_dir, 'getting-started.md'), 'w').close()
             open(os.path.join(tmp_dir, 'about.md'), 'w').close()
@@ -228,8 +228,6 @@ class ConfigTests(unittest.TestCase):
                     os.path.join('subC', 'index.md')
                 ]}
             ], conf['pages'])
-        finally:
-            shutil.rmtree(tmp_dir)
 
     def test_doc_dir_in_site_dir(self):
 
diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py
index 6fa4091a..ac2c3983 100644
--- a/mkdocs/tests/plugin_tests.py
+++ b/mkdocs/tests/plugin_tests.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
 
 import unittest
 import mock
+import os
 
 from mkdocs import plugins
 from mkdocs import utils
@@ -14,7 +15,8 @@ from mkdocs import config
 class DummyPlugin(plugins.BasePlugin):
     config_scheme = (
         ('foo', config.config_options.Type(utils.string_types, default='default foo')),
-        ('bar', config.config_options.Type(int, default=0))
+        ('bar', config.config_options.Type(int, default=0)),
+        ('dir', config.config_options.Dir(exists=False)),
     )
 
     def on_pre_page(self, content, **kwargs):
@@ -29,18 +31,27 @@ class DummyPlugin(plugins.BasePlugin):
 class TestPluginClass(unittest.TestCase):
 
     def test_valid_plugin_options(self):
+        test_dir = 'test'
 
         options = {
-            'foo': 'some value'
+            'foo': 'some value',
+            'dir': test_dir,
         }
 
+        cfg_fname = os.path.join('tmp', 'test', 'fname.yml')
+        cfg_fname = os.path.abspath(cfg_fname)
+
+        cfg_dirname = os.path.dirname(cfg_fname)
+        expected = os.path.join(cfg_dirname, test_dir)
+
         expected = {
             'foo': 'some value',
-            'bar': 0
+            'bar': 0,
+            'dir': expected,
         }
 
         plugin = DummyPlugin()
-        errors, warnings = plugin.load_config(options)
+        errors, warnings = plugin.load_config(options, config_file_path=cfg_fname)
         self.assertEqual(plugin.config, expected)
         self.assertEqual(errors, [])
         self.assertEqual(warnings, [])
@@ -132,7 +143,8 @@ class TestPluginConfig(unittest.TestCase):
         self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin)
         expected = {
             'foo': 'default foo',
-            'bar': 0
+            'bar': 0,
+            'dir': None,
         }
         self.assertEqual(cfg['plugins']['sample'].config, expected)
 
@@ -154,7 +166,8 @@ class TestPluginConfig(unittest.TestCase):
         self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin)
         expected = {
             'foo': 'foo value',
-            'bar': 42
+            'bar': 42,
+            'dir': None,
         }
         self.assertEqual(cfg['plugins']['sample'].config, expected)
 
@@ -194,7 +207,8 @@ class TestPluginConfig(unittest.TestCase):
         self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin)
         expected = {
             'foo': 'default foo',
-            'bar': 0
+            'bar': 0,
+            'dir': None,
         }
         self.assertEqual(cfg['plugins']['sample'].config, expected)