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:
I think that is a reasonable compromise. Not sure when I would want to point to a config file in a different location anyway. If I had multiple config files for one project, they would most likley be side-by-side in the root of the project anyway:

```
./docs/
./default.yml
./experimental.yml
```

Or I suppose the various config files could be in a subdir:

```
./docs/
./mkdocs_cfg/default.yml
./mkdocs_cfg/experimental.yml
```

Either way, I would be calling the `mkdocs` command from the project root. Therefore, it seems logical that all paths in the config would be relative to the cwd. Besides, if I rearranged my file structure from one to the other, I would not want to have to edit each file to alter the paths.

When automating doc builds (in a tox config or from a make file) one may need to cd into the project root before building, but that is nothing unusual (and good practice anyway).

Maybe the commands could accept a `--project_root` option which defaults to the cwd. That might address the few edge cases where someone is not calling a command from the project root. But is seems unnecessary to me.

Okay, fair point. I mostly wanted to raise this and have some sort of record of it. I think I agree, so, closing this for now :)

> Not sure when I would want to point to a config file in a different location anyway.

The case I care about (from issue #676) is when performing out-of-tree builds. I treat my build system as a toolbox of commands, and being able to run `make doc` (from the build dir, of course) is nice. The main reason I use out-of-tree builds is to make it easy to keep the build files for multiple build configs around (e.g. a clang build vs a gcc build).

While MkDocs itself is unlikely to benefit from from out-of-tree builds, it means that for my projects, I'm either forced to run mkdocs directly[1] or handle `cd`ing into the source dir to build the docs. Neither is particularly convenient, and I think the stated benefit (being able to put your mkdocs.yml file in a subdir without changing the relative paths) is of dubious merit. In fact, not knowing about this behavior, I'd probably end up changing the relative paths and then have to change them back when I found out that that's not what MkDocs expected.

[1] Which is more typing, since I prefer to use `--clean` and activate my MkDocs virtualenv in-line.

Yeah, so it is interesting that you hit this when doing automation like I did. I think we should try and fix this, I can't really see how the paths being relative to the user makes sense. Personally, I found it really easy to work around, I don't think it is that hard, but the fact that the config essentially changes as you move around the file-system is weird.

I think for this to work well, we would need to join each config option with the path to the mkdocs yaml when we do all the config processing in https://github.com/mkdocs/mkdocs/blob/master/mkdocs/config/config_options.py

Just agreeing that I think this is a valid issue

So, rather than:

```
cd /path/to/project && mkdocs build --clean
```

You'd prefer to use the following line in your automation scripts:

```
mkdocs build --clean --config-file /path/to/project/mkdocs.yml
```

Where the paths in the config file are presumably relative to the config file.

Sorry, but it is not clear to me how this is better. For that matter, it is not clear how the current way is better either (even if it is a few less characters of typing). What is clear is that it is different, which means that many existing users will need to change there config files if a change is made. And for what benefit?

I don't see why using `cd` isn't an acceptable solution.  Instead of recalculating all paths inside mkdocs for this specific situation (which may or may not be a lot more work), just realizing it needs to be in the cwd and use `cd` should be enough.  Just about any scripting language you would use to automate this stuff can change its cwd, IMO that should be enough, but I guess if the effort was put in to make it work the other way, that would be cool too, but I am okay with needing `cd`.

A very simple solution would be for MkDocs to to change it's cwd to the config file.

```
os.chdir(os.path.dirname(config_path))
```

This is a nasty nasty hack tho'

Def don't do that. ^ :)

@waylan: Well, given the build system I use, it's more like:

```
command('doc', cmd=['mkdocs', 'build', '--clean', source_file('mkdocs.yml')])
```

vs

```
srcdir = ... # Not actually sure what I'd do here.
command('doc', cmd="cd '{}' && mkdocs build --clean".format(shlex.quote(srcdir)))
```

Even the above isn't really perfect, since it'd break if the user was running the build system under Python 2.x. It's also something that took me a while to figure out, since I wasn't even thinking about the current-directory issue. (Granted, some of this is the fault of the build system making it somewhat difficult to `cd` into the srcdir.)

I agree that it's not the world's biggest issue, but it's a rough corner that makes things less convenient, when the only real benefit I can see is that it keeps the code somewhat simpler. I could probably patch the build system to do this in a slightly cleaner way if this bug were wontfixed, but I still think it would be a nice fix for MkDocs. Maybe it's more of a philosophical argument, but I've never liked it when programs unnecessarily depend on the current directory. It usually just ends up confusing me when I run into an error (I rarely think about what the cwd will be when I'm writing an automation script).

> This seems like a bit of a hack tho'

I've seen this done before, and I guess I've been guilty of doing it myself.  I'm not sure I would call it a hack if nothing else cares about the actual current working directory though.   I really see no problem with controlling your current working directory if you have a good reason to.  It would be completely transparent to the user, and it seems like Mkdocs doesn't care about the **actual** cwd.  You could always restore the actual cwd when Mkdocs is done.  If you aren't changing the cwd, you are just going to have to pass around Mkdocs calculated working directory, and then make everything relative to that.  Seems like a lot more work to me when `os.chdir(os.path.dirname(config_path))` would essentially solve all your problems.

Now if you needed to track the actual cwd **and** Mkdoc's cwd directory for some reason, then yeah, this is not an ideal solution and it should probably be handled differently by passing around your paths.

Meh, that's just my thoughts.

> You could always restore the actual cwd when Mkdocs is done.

Not sure if that would even be necessary, since a child process can't change its parent's cwd (which is why `cd` is an intrinsic shell function, not a real executable).

If Mkdocs was a library, then it would, but Mkdocs isn't really a lib, so your probably right.

> A very simple solution would be for MkDocs to to change it's cwd to the config file.

That would not help at all. For people who have a setup in which the cwd is not the directory in which the config file resides, this would break their setup the same way as making all paths relative to the config file's path. So, yeah, it would be a hack. Don't do that.

> this would break their setup the same way as making all paths relative to the config file's path.

I think that's the point. Since MkDocs isn't 1.0 yet, you can't really expect the config file's semantics to remain 100% set in stone.

> That would not help at all. For people who have a setup in which the cwd is not the directory in which the config file resides, this would break their setup the same way as making all paths relative to the config file's path.

Ahh, then yeah, if that is a concern, then it is a problem.  I'm not sure of all the ways it is being used and ultimately what is planned for final usage in 1.0.

> I think that's the point. Since MkDocs isn't 1.0 yet, you can't really expect the config file's semantics to remain 100% set in stone.

True, but it depends on Mkdoc's goals, which I am unaware of in regards to this.

> If Mkdocs was a library, then it would, but Mkdocs isn't really a lib, so your probably right.

While not a library, we tread into that territory once we have a plugin API. So, yeah, I don't think we want to change the cwd.

>  True, but it depends on Mkdoc's goals, which I am unaware of in regards to this.

Our goal is not to break things unless we really have to :) However, we should also take advantage of major releases to tidy up things that are not right. Incidentally, we should formally come up with a backwards compatability policy before we hit 1.0. 

Having said all that, I think the current behaviour isn't correct. So we should look at changing it for a 1.0 release, assuming it isn't going to be that difficult. We could start to introduce it earlier with a config setting to toggle it on and then remove that flag in 1.0 

Okay, so here is my concern: There are a lot of MkDocs users. In fact, according to [this comment](https://github.com/rtfd/readthedocs.org/pull/1305#issuecomment-107232177), over 2000 projects are hosted on Read The Docs that use MkDocs. If even 10% of those projects use non-standard setups (I have no data regarding this), that's 200 projects that potentially need to change their config files. And Read The Docs is certainly not the entire user base.

Yes, this is pre-1.0 software with no backward-compatibility guarantee. In fact, a recent revision required the `pages` config option to be completely refactored. The difference is that the old format can easily be detected and a stub was created which converted the old format to the new one with a deprecation warning being issued.

However, if the paths in the config file are changed from being relative to the cwd to being relative to the config file's path (an easy enough change to make), there will be no way to detect whether users have updated their config files, adjust accordingly (avoiding a failure to build)  and issue a warning. Instead, those projects will just break hard. Sure a well crafted error message could be created to point them in the right direction, but there will be no deprecation path.

I can't find it now, but I recall seeing a comment on some issue previously which indicated that the developers of MkDocs were aware that a significant number of users have non-standard setups which would break if this change it being made. Specifically, a number of users have placed their config file inside their docs file. However, they call the `mkdocs` command from the parent dir. Every one of those users projects would break if this change was made.

To be clear, I am not one of those users. Nothing would break for me. I am simply asking if the backward-compatibility issue is enough to warrant leaving this as-is, especially given the inability to do a graceful deprecation.

If that issue did't exist, I would actually prefer the change myself. It is certainly less surprising. However, I don't think it is important enough to justify the breakage. But, of course, I'm not a developer of this project, so the call is not mine.

I don't think it's actually that hard to detect if the semantics change would break your build, assuming we care enough to detect it. Some observations:
- This will only change behavior if your `mkdocs.yml` is _not_ in your cwd.
- If your `mkdocs.yml` is in another directory, there are exactly two base directories you need to check: your cwd and the directory containing your `mkdocs.yml`.
- The `docs_dir` is probably unique enough that you're unlikely to have a directory of that name in both your cwd and your `mkdocs.yml` dir.

Therefore, you should be able to get pretty darn close to automatically detecting all cases where this would change. Just check both the cwd and the `mkdocs.yml` dir for the existence of the `docs_dir`:
- If it's in the `mkdocs.yml` dir, then hooray, you're done!
- If it's in the cwd, issue a warning (and build anyway?).
- If it's in both (which is pretty unlikely), error out, and maybe add a `--force` option to use the new semantics. That way, you're not completely out of luck, but you've still learned about the new behavior.

Eventually, say at 1.0, you can drop all of this logic and always use the `mkdocs.yml` dir.

> There are a lot of MkDocs users. In fact, according to this comment, over 2000 projects are hosted on Read The Docs that use MkDocs.

The slight irony of using ReadTheDocs as an example is that users have no control over the CWD in that example. So the documentation should build consistently on RTD's and however you build it elsewhere. This wont happen if the user requires something specific like calling from the parent.

I do believe that RTD is a large proportion of ours users, many people come to us as they want to use RTD but don't want to learn Sphinx/reStructuredText.

> However, if the paths in the config file are changed from being relative to the cwd to being relative to the config file's path (an easy enough change to make), there will be no way to detect whether users have updated their config files, adjust accordingly (avoiding a failure to build) and issue a warning.

I can see a couple of options.
- If we can't find anything specified in the config, look for them relative to CWD and build with that and display a warning.
- Add a config setting called `paths_relative_to_config` because I can't think of a name yet, that defaults to False in the next release but with a warning stating that the default will be changing in 1.0 and the setting will be removed. Users then have time to upgrade as they see fit. This is mostly going to be annoying for users that otherwise wouldn't need to do anything - as they would need to add a setting and remove it later.
- ... and I started to write about detecting it if the config isn't in your CWD but @jimporter beat me.

> Specifically, a number of users have placed their config file inside their docs file. However, they call the mkdocs command from the parent dir. Every one of those users projects would break if this change was made.

This sounds like how lots of users do it on ReadTheDocs, they don't call from the parent tho' - often the root of the git repo is the docs_dir. So the config is within the docs_dir, that caused an annoying bug :)

> If that issue did't exist, I would actually prefer the change myself. It is certainly less surprising. 

I believe that one of the biggest goals of MkDocs is to have sensible defaults and be as unsurprising as possible. If we don't change these things early on, then they will only get harder to change later.

All things being equal, I'd vote for detecting as much non-standardness as possible, complaining loudly if any is found, and enforcing as minimal-as-possible set of different-ways-to-do-a-thing headed into 1.0.

> If we don't change these things early on, then they will only get harder to change later.

Hear hear!  Now is the moment.

Alright then. Sounds like the consensus is that the breaking change is worth it.

Please, just don't use the `paths_relative_to_config` config setting (or any better named config setting). If we can either fall back to the cwd or detect if the config file is not in the cwd and adapt and issue a warning that would be a sensible middle ground.

> Please, just don't use the paths_relative_to_config config setting (or any better named config setting)

I won't. If that is required to make it happen I might be swayed against the change.

Alternative: remove `--config-file`. Because why?

> Alternative: remove `--config-file`. Because why?

You may have a point. But I suspect you will get more complaints from people by making that change than anything else. And it will be a never-ending feature request.

> Alternative: remove --config-file. Because why?

The downside of this is that it's more complex for my case: I want to build documentation from my build dir, and then put the HTML in a subdirectory of the build dir. This makes cleaning a build trivial: `rm -rf build`. Without `--config-file` (and this issue), I'd have to both `cd` into the srcdir, _and_ set the `--site-dir`. With them, I could just set `--config-file`.

A strong +1 for making the paths relative to config - or at least add whatever extra option to enable that. I sure as hell don't want to cd back and forth. Cding to the docs dir prior to mkdocs build may sound reasonable when you compare just the command lines to run mkdocs, but that is an insufficient comparison. I would think anyone spending a lot of time in unix shells doing varying tasks in various apps will understand that. Having to juggle between dirs is a pain, even with push & popd.

I am using mkdocs to generate API documentation for a Python package. In my case, the mkdocs sources are using Jinja templating and are kept within the package. So I run a sort of preprocessor (Jinja etc) to generate "static" mkdocs sources outside the package tree. Including mkdocs.yml. To work around the current situation, I have to hardcode paths in the preprocessor step to be able to properly run the final mkdocs build step in the end. Which feels insane.

At some point I will just use mkdocs as a library, so I hope I will be able to sidestep the issue, but not there yet...
Ironically (as I've been the voice against this change), I just came up against a need for this feature. If you are deploying to user/organization GitHub pages, you need this. User/org pages do not commit to the `gh-pages` branch, but the master branch of a separate repo from the project. Currently the `mkdocs gh-deploy` command won't work. You need to use `ghp-import` directly like this:

```bash
cd project-repo
mkdocs build
cd ../pages-repo
ghp-import -b master -p ../project-repo/site 
```

If paths in the config were relative to the config file rather than the CWD, then this would work:

```
cd pages-repo
mkdocs gh-deploy -f ../project-repo/mkdocs.yml -b master
```

But as long as paths are relative to the CWD, that will never work. And as you need to run the command from the repo you are committing to, `cd`ing into the project is not an option as that takes you out of the repo.

I think this is the use-case that pulls me over. We now have a reason to make the change which overcomes my previous objections.
@waylan That's a really good point, and something I should take into account for [mike](https://github.com/jimporter/mike) as well.

Resolving patch:
diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py
--- 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
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -293,6 +293,24 @@ def __init__(self, exists=False, **kwargs):
         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 @@ def post_validation(self, config, key_name):
 
         # 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 @@ def run_validation(self, value):
                 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 @@ def load_plugin(self, name, config):
                 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
--- 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()


Test patch:
diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py
--- 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 @@
     # 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 @@ def test_markdown_duplicate_custom_extension(self):
         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 @@ def test_copying_media(self):
             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 @@ def test_copy_theme_files(self):
             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 @@ def test_extra_context(self):
         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 @@ def test_BOM(self):
             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
--- a/mkdocs/tests/config/base_tests.py
+++ b/mkdocs/tests/config/base_tests.py
@@ -3,6 +3,12 @@
 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 @@ def test_load_from_file(self):
         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 @@ def test_load_from_open_file(self):
         `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 @@ def test_load_from_open_file(self):
             # 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 @@ def test_load_from_closed_file(self):
         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 @@ def test_load_from_closed_file(self):
             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 @@ def post_validation(self, config, key_name):
             ('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
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -6,6 +6,7 @@
 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 @@ def test_incorrect_type_type_error(self):
                           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 @@ def validate_config(self, config):
         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
--- 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 @@ def test_config_option(self):
         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 @@ def test_config_option(self):
             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 @@ def test_default_pages(self):
             })
             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 @@ def test_default_pages_nested(self):
                     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
--- a/mkdocs/tests/plugin_tests.py
+++ b/mkdocs/tests/plugin_tests.py
@@ -5,6 +5,7 @@
 
 import unittest
 import mock
+import os
 
 from mkdocs import plugins
 from mkdocs import utils
@@ -14,7 +15,8 @@
 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 @@ def on_nav(self, item, **kwargs):
 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 @@ def test_plugin_config_without_options(self, mock_class):
         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 @@ def test_plugin_config_with_options(self, mock_class):
         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 @@ def test_plugin_config_none_with_default(self, mock_class):
         self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin)
         expected = {
             'foo': 'default foo',
-            'bar': 0
+            'bar': 0,
+            'dir': None,
         }
         self.assertEqual(cfg['plugins']['sample'].config, expected)