Repository: translate/pootle

Issue:
Allow language exclusion in pootle_fs project config
Atm you cant specify a language as not being managed by pootle_fs

relatedly we have a plan to allow tp-level fs config



Discussion:

Resolving patch:
diff --git a/pootle/apps/pootle_fs/finder.py b/pootle/apps/pootle_fs/finder.py
--- a/pootle/apps/pootle_fs/finder.py
+++ b/pootle/apps/pootle_fs/finder.py
@@ -31,12 +31,14 @@ class TranslationFileFinder(object):
     extensions = DEFAULT_EXTENSIONS
     path_mapping = PATH_MAPPING
 
-    def __init__(self, translation_mapping, path_filters=None, extensions=None):
+    def __init__(self, translation_mapping, path_filters=None, extensions=None,
+                 exclude_languages=None):
         TranslationMappingFinderValidator(translation_mapping).validate()
         self.translation_mapping = translation_mapping
         if extensions:
             self.extensions = extensions
         self.path_filters = path_filters
+        self.exclude_languages = exclude_languages or []
 
     @cached_property
     def regex(self):
@@ -60,6 +62,8 @@ def match(self, file_path):
         if not match:
             return
         matched = match.groupdict()
+        if matched["language_code"] in self.exclude_languages:
+            return
         matched["dir_path"] = matched.get("dir_path", "").strip("/")
         if not matched.get("filename"):
             matched["filename"] = os.path.splitext(
@@ -85,6 +89,8 @@ def reverse_match(self, language_code, filename=None,
         """For given matchdata return the file path that would be
         matched.
         """
+        if language_code in self.exclude_languages:
+            return
         if extension is None:
             extension = self.extensions[0]
         if extension not in self.extensions:
diff --git a/pootle/apps/pootle_fs/matcher.py b/pootle/apps/pootle_fs/matcher.py
--- a/pootle/apps/pootle_fs/matcher.py
+++ b/pootle/apps/pootle_fs/matcher.py
@@ -31,6 +31,15 @@ def lang_mapper(self):
             self.project.__class__,
             instance=self.project)
 
+    @cached_property
+    def excluded_languages(self):
+        excluded = self.project.config.get(
+            "pootle.fs.excluded_languages")
+        return [
+            self.lang_mapper.get_upstream_code(code) or code
+            for code
+            in excluded or []]
+
     @lru_cache(maxsize=None)
     def get_finder(self, fs_path=None):
         path_filters = []
@@ -39,7 +48,8 @@ def get_finder(self, fs_path=None):
         return self.context.finder_class(
             self.translation_mapping,
             extensions=self.project.filetype_tool.valid_extensions,
-            path_filters=path_filters)
+            path_filters=path_filters,
+            exclude_languages=self.excluded_languages)
 
     @property
     def project(self):
diff --git a/pootle/apps/pootle_fs/resources.py b/pootle/apps/pootle_fs/resources.py
--- a/pootle/apps/pootle_fs/resources.py
+++ b/pootle/apps/pootle_fs/resources.py
@@ -28,10 +28,19 @@ def __str__(self):
             % (self.__class__.__name__,
                self.project))
 
+    @property
+    def excluded_languages(self):
+        return self.project.config.get("pootle.fs.excluded_languages")
+
     @property
     def stores(self):
-        return Store.objects.filter(
+        excluded = self.excluded_languages
+        stores = Store.objects.filter(
             translation_project__project=self.project)
+        if excluded:
+            stores = stores.exclude(
+                translation_project__language__code__in=excluded)
+        return stores
 
     @property
     def tracked(self):


Test patch:
diff --git a/tests/pootle_fs/finder.py b/tests/pootle_fs/finder.py
--- a/tests/pootle_fs/finder.py
+++ b/tests/pootle_fs/finder.py
@@ -211,3 +211,23 @@ def test_finder_match(finder_matches):
 def test_finder_find(fs_finder):
     finder, expected = fs_finder
     assert sorted(expected) == sorted(f for f in finder.find())
+
+
+@pytest.mark.django_db
+@pytest.mark.xfail(sys.platform == 'win32',
+                   reason="path mangling broken on windows")
+def test_finder_exclude_langs():
+    finder = TranslationFileFinder(
+        "/path/to/<dir_path>/<language_code>.<ext>",
+        exclude_languages=["foo", "bar"])
+    assert not finder.match("/path/to/foo.po")
+    assert not finder.match("/path/to/bar.po")
+    match = finder.match("/path/to/baz.po")
+    assert match[0] == "/path/to/baz.po"
+    assert match[1]["language_code"] == "baz"
+
+    assert not finder.reverse_match(language_code="foo")
+    assert not finder.reverse_match(language_code="bar")
+    assert (
+        finder.reverse_match(language_code="baz")
+        == "/path/to/baz.po")
diff --git a/tests/pootle_fs/matcher.py b/tests/pootle_fs/matcher.py
--- a/tests/pootle_fs/matcher.py
+++ b/tests/pootle_fs/matcher.py
@@ -42,6 +42,10 @@ def dummy_paths(self):
         paths = []
         for pootle_path in self.pootle_paths:
             match = resolve(pootle_path).kwargs
+            excluded_languages = self.project.config.get(
+                "pootle.fs.excluded_languages", [])
+            if match["language_code"] in excluded_languages:
+                continue
             match["filename"], match["ext"] = os.path.splitext(
                 match["filename"])
             match["ext"] = match["ext"][1:]
@@ -347,3 +351,39 @@ def test_matcher_language_mapper_bad(settings):
     assert "language1_FOO" not in matcher.lang_mapper
     assert matcher.lang_mapper["language1_FOO"] is None
     assert matcher.lang_mapper["language1"] == language1
+
+
+@pytest.mark.django_db
+@pytest.mark.xfail(sys.platform == 'win32',
+                   reason="path mangling broken on windows")
+def test_matcher_matches_excluded_langs(settings, caplog, project0, language0):
+    settings.POOTLE_FS_WORKING_PATH = os.sep.join(['', 'path', 'to'])
+    project0.config["pootle_fs.translation_mappings"] = dict(
+        default="/some/other/path/<language_code>/<dir_path>/<filename>.<ext>")
+    project0.config["pootle.fs.excluded_languages"] = [language0.code]
+    matcher = FSPathMatcher(DummyContext(project0))
+    assert matcher.excluded_languages == [language0.code]
+    assert matcher.get_finder().exclude_languages == [language0.code]
+    assert not any(
+        m[0].startswith("/%s/" % language0.code)
+        for m in matcher.matches(None, None))
+
+
+@pytest.mark.django_db
+@pytest.mark.xfail(sys.platform == 'win32',
+                   reason="path mangling broken on windows")
+def test_matcher_matches_excluded_mapped_langs(settings, caplog,
+                                               project0, language0):
+    settings.POOTLE_FS_WORKING_PATH = os.sep.join(['', 'path', 'to'])
+    project0.config["pootle_fs.translation_mappings"] = dict(
+        default="/some/other/path/<language_code>/<dir_path>/<filename>.<ext>")
+    project0.config["pootle.fs.excluded_languages"] = [language0.code]
+    project0.config["pootle.core.lang_mapping"] = OrderedDict(
+        [["%s_FOO" % language0.code, language0.code]])
+    matcher = FSPathMatcher(DummyContext(project0))
+    assert matcher.excluded_languages == ["%s_FOO" % language0.code]
+    assert matcher.get_finder().exclude_languages == ["%s_FOO" % language0.code]
+    assert not any(
+        m[0].startswith("/%s/" % language0.code)
+        for m in matcher.matches(None, None))
+    assert matcher.matches(None, None)
diff --git a/tests/pootle_fs/resources.py b/tests/pootle_fs/resources.py
--- a/tests/pootle_fs/resources.py
+++ b/tests/pootle_fs/resources.py
@@ -30,18 +30,26 @@ def test_project_resources_instance():
 
 
 @pytest.mark.django_db
-def test_project_resources_stores():
-    project = Project.objects.get(code="project0")
+def test_project_resources_stores(project0, language0):
     stores = Store.objects.filter(
-        translation_project__project=project)
-    assert list(FSProjectResources(project).stores) == list(stores)
+        translation_project__project=project0)
+    assert list(FSProjectResources(project0).stores) == list(stores)
     # mark some Stores obsolete - should still show
     store_count = stores.count()
     assert store_count
     for store in stores:
         store.makeobsolete()
-    assert list(FSProjectResources(project).stores) == list(stores)
+    assert list(FSProjectResources(project0).stores) == list(stores)
     assert stores.count() == store_count
+    project0.config["pootle.fs.excluded_languages"] = [language0.code]
+    filtered_stores = stores.exclude(
+        translation_project__language=language0)
+    assert (
+        list(FSProjectResources(project0).stores)
+        != list(stores))
+    assert (
+        list(FSProjectResources(project0).stores)
+        == list(filtered_stores))
 
 
 @pytest.mark.django_db