Repository: sphinx-doc/sphinx

Issue:
Sphinx crashes if unknown po file exists
### Problem
- Sphinx crashes if unknown po file exists

#### Procedure to reproduce the problem
```
$  sphinx-quickstart -q -aa -pp test
$  cd test
$  mkdir -p locales/en/LC_MESSAGES/sphinx
$  touch locales/en/LC_MESSAGES/sphinx/index.po
$  make html SPHINXOPTS='-D language=en'
```

#### Error logs / results
```
# Sphinx version: 2.0.0+
# Python version: 3.6.5 (CPython)
# Docutils version: 0.14
# Jinja2 version: 2.10
# Last messages:
#   loading translations [en]...
#
#   done
#
#   making output directory...
#
#   building [mo]: targets for 1 po files that are out of date
#
#   writing output... [100%] locales/en/LC_MESSAGES/sphinx.mo
#
# Loaded extensions:
#   sphinx.ext.mathjax (2.0.0+) from /Users/tkomiya/work/sphinx/sphinx/ext/mathjax.py
#   alabaster (0.7.10) from /Users/tkomiya/work/tmp/lib/python3.6/site-packages/alabaster-0.7.10-py3.6.egg/alabaster/__init__.py
Traceback (most recent call last):
  File "/Users/tkomiya/work/sphinx/sphinx/cmd/build.py", line 304, in build_main
    app.build(args.force_all, filenames)
  File "/Users/tkomiya/work/sphinx/sphinx/application.py", line 340, in build
    self.builder.compile_update_catalogs()
  File "/Users/tkomiya/work/sphinx/sphinx/builders/__init__.py", line 281, in compile_update_catalogs
    self.compile_catalogs(catalogs, message)
  File "/Users/tkomiya/work/sphinx/sphinx/builders/__init__.py", line 236, in compile_catalogs
    catalog.write_mo(self.config.language)
  File "/Users/tkomiya/work/sphinx/sphinx/util/i18n.py", line 71, in write_mo
    with io.open(self.po_path, 'rt', encoding=self.charset) as file_po:
FileNotFoundError: [Errno 2] No such file or directory: '/Users/tkomiya/work/tmp/doc/test/locales/en/LC_MESSAGES/sphinx.po'
```

#### Expected results
Sphinx skips them and goes builds fine.

### Environment info
- OS: Mac
- Python version: 3.7.0
- Sphinx version: 2.0.0+


Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index e33793525..0593c478d 100644
--- a/CHANGES
+++ b/CHANGES
@@ -17,6 +17,8 @@ Deprecated
 
 * ``sphinx.io.SphinxI18nReader.set_lineno_for_reporter()`` is deprecated
 * ``sphinx.io.SphinxI18nReader.line`` is deprecated
+* ``sphinx.util.i18n.find_catalog_source_file()`` has changed; the
+  *gettext_compact* argument has been deprecated
 
 Features added
 --------------
@@ -41,6 +43,7 @@ Bugs fixed
 * #2720, #4034: Incorrect links with ``:download:``, duplicate names, and
   parallel builds
 * #5290: autodoc: failed to analyze source code in egg package
+* #5399: Sphinx crashes if unknown po file exists
 
 Testing
 --------
diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst
index 13a08e49b..6af027dd8 100644
--- a/doc/extdev/index.rst
+++ b/doc/extdev/index.rst
@@ -136,6 +136,12 @@ The following is a list of deprecated interface.
      - 4.0
      - :confval:`autodoc_default_options`
 
+   * - ``gettext_compact`` arguments of
+       ``sphinx.util.i18n.find_catalog_source_files()``
+     - 1.8
+     - 3.0
+     - N/A
+
    * - ``sphinx.io.SphinxI18nReader.set_lineno_for_reporter()``
      - 1.8
      - 3.0
diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py
index e446ca94a..9c7537631 100644
--- a/sphinx/builders/__init__.py
+++ b/sphinx/builders/__init__.py
@@ -261,7 +261,6 @@ def compile_all_catalogs(self):
             [path.join(self.srcdir, x) for x in self.config.locale_dirs],
             self.config.language,
             charset=self.config.source_encoding,
-            gettext_compact=self.config.gettext_compact,
             force_all=True,
             excluded=Matcher(['**/.?**']))
         message = __('all of %d po files') % len(catalogs)
@@ -284,7 +283,6 @@ def to_domain(fpath):
             self.config.language,
             domains=list(specified_domains),
             charset=self.config.source_encoding,
-            gettext_compact=self.config.gettext_compact,
             excluded=Matcher(['**/.?**']))
         message = __('targets for %d po files that are specified') % len(catalogs)
         self.compile_catalogs(catalogs, message)
@@ -295,7 +293,6 @@ def compile_update_catalogs(self):
             [path.join(self.srcdir, x) for x in self.config.locale_dirs],
             self.config.language,
             charset=self.config.source_encoding,
-            gettext_compact=self.config.gettext_compact,
             excluded=Matcher(['**/.?**']))
         message = __('targets for %d po files that are out of date') % len(catalogs)
         self.compile_catalogs(catalogs, message)
diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py
index 106455302..edaebebda 100644
--- a/sphinx/util/i18n.py
+++ b/sphinx/util/i18n.py
@@ -12,6 +12,7 @@
 import io
 import os
 import re
+import warnings
 from collections import namedtuple
 from datetime import datetime
 from os import path
@@ -20,6 +21,7 @@
 from babel.messages.mofile import write_mo
 from babel.messages.pofile import read_po
 
+from sphinx.deprecation import RemovedInSphinx30Warning
 from sphinx.errors import SphinxError
 from sphinx.locale import __
 from sphinx.util import logging
@@ -103,7 +105,7 @@ def find_catalog_files(docname, srcdir, locale_dirs, lang, compaction):
     return files  # type: ignore
 
 
-def find_catalog_source_files(locale_dirs, locale, domains=None, gettext_compact=False,
+def find_catalog_source_files(locale_dirs, locale, domains=None, gettext_compact=None,
                               charset='utf-8', force_all=False,
                               excluded=Matcher([])):
     # type: (List[unicode], unicode, List[unicode], bool, unicode, bool, Matcher) -> Set[CatalogInfo]  # NOQA
@@ -115,14 +117,15 @@ def find_catalog_source_files(locale_dirs, locale, domains=None, gettext_compact
     :param str locale: a language as `'en'`
     :param list domains: list of domain names to get. If empty list or None
        is specified, get all domain names. default is None.
-    :param boolean gettext_compact:
-       * False: keep domains directory structure (default).
-       * True: domains in the sub directory will be merged into 1 file.
     :param boolean force_all:
        Set True if you want to get all catalogs rather than updated catalogs.
        default is False.
     :return: [CatalogInfo(), ...]
     """
+    if gettext_compact is not None:
+        warnings.warn('gettext_compact argument for find_catalog_source_files() '
+                      'is deprecated.', RemovedInSphinx30Warning)
+
     catalogs = set()  # type: Set[CatalogInfo]
 
     if not locale:
@@ -143,10 +146,7 @@ def find_catalog_source_files(locale_dirs, locale, domains=None, gettext_compact
                 if excluded(path.join(relpath(dirpath, base_dir), filename)):
                     continue
                 base = path.splitext(filename)[0]
-                domain = relpath(path.join(dirpath, base), base_dir)
-                if gettext_compact and path.sep in domain:
-                    domain = path.split(domain)[0]
-                domain = domain.replace(path.sep, SEP)
+                domain = relpath(path.join(dirpath, base), base_dir).replace(path.sep, SEP)
                 if domains and domain not in domains:
                     continue
                 cat = CatalogInfo(base_dir, domain, charset)


Test patch:
diff --git a/tests/test_util_i18n.py b/tests/test_util_i18n.py
index da8abe265..b7f460cd7 100644
--- a/tests/test_util_i18n.py
+++ b/tests/test_util_i18n.py
@@ -154,7 +154,7 @@ def test_get_catalogs_with_compact(tempdir):
 
     catalogs = i18n.find_catalog_source_files([tempdir / 'loc1'], 'xx', gettext_compact=True)
     domains = set(c.domain for c in catalogs)
-    assert domains == set(['test1', 'test2', 'sub'])
+    assert domains == set(['test1', 'test2', 'sub/test3', 'sub/test4'])
 
 
 def test_get_catalogs_excluded(tempdir):