Repository: spotify/luigi

Issue:
Inconsistent filename references to LocalTarget and MockTarget
LocalTargets and MockTargets are referenced differently.

``` python
import luigi
from luigi import mock

class MockHelloWork(luigi.Task):
    def requires(self):
        return [Hello()]
    def output(self):
        return mock.MockTarget("dummy_file")
    def run(self):
        with open(self.output()._fn, 'w') as mockout1:  # MockTarget class variable _fn
            mockout1.write('done')
        with open(self.output().path, 'w') as mockout2:  # MockTarget property path
            mockout2.write('still done')

class Hello(luigi.Task):
    def requires(self):
        return [World()]
    def output(self):
        return luigi.LocalTarget("/path/to/file.txt")
    def run(self):
        with open(self.input()[0].path, 'w') as infile:  # LocalTarget class variable path
            indata = infile.read()
        with open(self.output().fn, 'w') as outfile:  # LocalTarget property fn
            outfile.write("Hello " + indata)

class World(luigi.Task):
    def output(self):
        return luigi.LocalTarget("/path/to/another/file.txt")
    def run(self):
        with open(self.output().path, 'w') as outfile:  # LocalTarget class variable path
            outfile.write("World")
```

|  | LocalTarget | MockTarget |
| :-- | :-: | :-: |
| class var | `path` | `_fn` |
| property | `fn` | `path` |

It seems like they should use the same property name and be referenced accordingly. The only internal cases where `output().[something]` are referenced in luigi are the three contrib modules: hadoop, scalding, and sge (within a docstring) - which all use `output().path` (this might not include all internal references to LocalTargets, but these were the only ones that resulted from `grep -r --exclude-dir=luigi/static/visualiser "output().path" luigi`). Thus, if MockTarget or LocalTarget are to be changed, I'd vote to alter the property name of LocalTarget to `path` and its class variable to `_fn` to match MockTarget. This way testing using MockTargets against modules which reference LocalTargets will be the same.

This was discovered when running `tox -e py27-nonhdfs test/contrib/salesforce_test.py` on a version of salesforce which referenced LocalTargets with the `fn` property. An error was thrown because `fn` does not exist as a property or class variable for MockTarget.

I do understand this would be a core luigi change to `File.py` that would likely affect a number of Luigi users.

Thoughts?



Discussion:
Do we even need for them to have `@property`-names? I find "fn" to be confusing (it sounds like "function" to me). Can we just deprecate `fn` (no need for  `_fn` as its private already) and just make "path" a class var for both classes?

We have also previously favored "path" over "fn". See https://github.com/spotify/luigi/pull/486, either, I'm up for any change consolidating things and doesn't break too much.

Also I think we can safely assume nobody relies on `MockTarget` for production behavior.

So if i'm understanding correct, the following things needs to happen:
- Add a decracated warning to property LocalTarget.fn
  - correct usage will be to reference the class variable LocalTarget.path
- Rename class variable MockTarget._fn to MockTarget.path, and replace class references to self._fn with self.path
- Remove property MockTarget.path

If this is correct, I'll submit a PR.

Yes. I think you covered everything. Looks good :)


Resolving patch:
diff --git a/luigi/file.py b/luigi/file.py
--- a/luigi/file.py
+++ b/luigi/file.py
@@ -154,6 +154,7 @@ def copy(self, new_path, raise_if_exists=False):
 
     @property
     def fn(self):
+        warnings.warn("Use LocalTarget.path to reference filename", DeprecationWarning, stacklevel=2)
         return self.path
 
     def __del__(self):
diff --git a/luigi/mock.py b/luigi/mock.py
--- a/luigi/mock.py
+++ b/luigi/mock.py
@@ -106,7 +106,7 @@ class MockTarget(target.FileSystemTarget):
 
     def __init__(self, fn, is_tmp=None, mirror_on_stderr=False, format=None):
         self._mirror_on_stderr = mirror_on_stderr
-        self._fn = fn
+        self.path = fn
         if format is None:
             format = get_default_format()
 
@@ -117,13 +117,13 @@ def __init__(self, fn, is_tmp=None, mirror_on_stderr=False, format=None):
         self.format = format
 
     def exists(self,):
-        return self._fn in self.fs.get_all_data()
+        return self.path in self.fs.get_all_data()
 
     def move(self, path, raise_if_exists=False):
         """
         Call MockFileSystem's move command
         """
-        self.fs.move(self._fn, path, raise_if_exists)
+        self.fs.move(self.path, path, raise_if_exists)
 
     def rename(self, *args, **kwargs):
         """
@@ -131,12 +131,8 @@ def rename(self, *args, **kwargs):
         """
         self.move(*args, **kwargs)
 
-    @property
-    def path(self):
-        return self._fn
-
     def open(self, mode):
-        fn = self._fn
+        fn = self.path
         mock_target = self
 
         class Buffer(BytesIO):


Test patch: