Repository: python-babel/babel
Pull request #438 (2016-08-09 08:47:21)

Add option 'add_location' for location line formatting
Support of add_location option added, can be either 'full', 'file'
or 'never'. 'full' includes both file and line number; 'file' includes
only the file name without line number; 'never' doesn't include the
location comment at all (same as --no-location).

Python babel implementation lacks of this option. We would like to use it to avoid merge conflicts while merging po files.

Option `add_location` is implemented in the GNU xgettext, quote from the manual:

> ‘--add-location=type’
> 
> ```
> Generate ‘#: filename:line’ lines (default).
> 
> The optional type can be either ‘full’, ‘file’, or ‘never’. If it is not given or ‘full’, it generates the lines with both file name and line number. If it is ‘file’, the line number part is omitted. If it is ‘never’, it completely suppresses the lines (same as --no-location).
> ```

https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html


codecov-io: ## [Current coverage](https://codecov.io/gh/python-babel/babel/pull/438?src=pr) is 90.15% (diff: 91.66%)
> Merging [#438](https://codecov.io/gh/python-babel/babel/pull/438?src=pr) into [master](https://codecov.io/gh/python-babel/babel/branch/master?src=pr) will decrease coverage by **<.01%**


```diff
@@             master       #438   diff @@
==========================================
  Files            24         24          
  Lines          3954       3963     +9   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits           3565       3573     +8   
- Misses          389        390     +1   
  Partials          0          0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [d277c12...5de9ee4](https://codecov.io/gh/python-babel/babel/compare/d277c12319ceeb6066898fca64a0381ca5f5a40c...5de9ee436c50a0cc61c89fa91652567e54a20af2?src=pr)

Patch:
diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py
index 3c94bf3..da4323c 100644
--- a/babel/messages/frontend.py
+++ b/babel/messages/frontend.py
@@ -104,6 +104,10 @@ class Command(_Command):
     #: that are usable with optparse.
     option_aliases = {}
 
+    #: Choices for options that needed to be restricted to specific
+    #: list of choices.
+    option_choices = {}
+
     #: Log object. To allow replacement in the script command line runner.
     log = distutils_log
 
@@ -273,6 +277,11 @@ class extract_messages(Command):
          'path to the mapping configuration file'),
         ('no-location', None,
          'do not include location comments with filename and line number'),
+        ('add-location', None,
+         'location lines format. If it is not given or "full", it generates '
+         'the lines with both file name and line number. If it is "file", '
+         'the line number part is omitted. If it is "never", it completely '
+         'suppresses the lines (same as --no-location).'),
         ('omit-header', None,
          'do not include msgid "" entry in header'),
         ('output-file=', 'o',
@@ -317,6 +326,9 @@ class extract_messages(Command):
         'output-file': ('--output',),
         'strip-comments': ('--strip-comment-tags',),
     }
+    option_choices = {
+        'add-location': ('full', 'file', 'never',),
+    }
 
     def initialize_options(self):
         self.charset = 'utf-8'
@@ -324,6 +336,7 @@ class extract_messages(Command):
         self.no_default_keywords = False
         self.mapping_file = None
         self.no_location = False
+        self.add_location = None
         self.omit_header = False
         self.output_file = None
         self.input_dirs = None
@@ -338,6 +351,7 @@ class extract_messages(Command):
         self.version = None
         self.add_comments = None
         self.strip_comments = False
+        self.include_lineno = True
 
     def finalize_options(self):
         if self.input_dirs:
@@ -401,6 +415,11 @@ class extract_messages(Command):
             if not self.version:
                 self.version = self.distribution.get_version()
 
+        if self.add_location == 'never':
+            self.no_location = True
+        elif self.add_location == 'file':
+            self.include_lineno = False
+
     def run(self):
         mappings = self._get_mappings()
         with open(self.output_file, 'wb') as outfile:
@@ -459,7 +478,8 @@ class extract_messages(Command):
                      no_location=self.no_location,
                      omit_header=self.omit_header,
                      sort_output=self.sort_output,
-                     sort_by_file=self.sort_by_file)
+                     sort_by_file=self.sort_by_file,
+                     include_lineno=self.include_lineno)
 
     def _get_mappings(self):
         mappings = []
@@ -859,14 +879,15 @@ class CommandLineInterface(object):
             if short:
                 strs.append("-%s" % short)
             strs.extend(cmdclass.option_aliases.get(name, ()))
+            choices = cmdclass.option_choices.get(name, None)
             if name == as_args:
                 parser.usage += "<%s>" % name
             elif name in cmdclass.boolean_options:
                 parser.add_option(*strs, action="store_true", help=help)
             elif name in cmdclass.multiple_value_options:
-                parser.add_option(*strs, action="append", help=help)
+                parser.add_option(*strs, action="append", help=help, choices=choices)
             else:
-                parser.add_option(*strs, help=help, default=default)
+                parser.add_option(*strs, help=help, default=default, choices=choices)
         options, args = parser.parse_args(argv)
 
         if as_args:
diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py
index e431fcd..741e25f 100644
--- a/babel/messages/pofile.py
+++ b/babel/messages/pofile.py
@@ -371,7 +371,7 @@ def normalize(string, prefix='', width=76):
 
 def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
              sort_output=False, sort_by_file=False, ignore_obsolete=False,
-             include_previous=False):
+             include_previous=False, include_lineno=True):
     r"""Write a ``gettext`` PO (portable object) template file for a given
     message catalog to the provided file-like object.
 
@@ -413,6 +413,7 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
                             comments
     :param include_previous: include the old msgid as a comment when
                              updating the catalog
+    :param include_lineno: include line number in the location comment
     """
     def _normalize(key, prefix=''):
         return normalize(key, prefix=prefix, width=width)
@@ -486,7 +487,7 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
         if not no_location:
             locs = []
             for filename, lineno in sorted(message.locations):
-                if lineno:
+                if lineno and include_lineno:
                     locs.append(u'%s:%d' % (filename.replace(os.sep, '/'), lineno))
                 else:
                     locs.append(u'%s' % filename.replace(os.sep, '/'))


Test patch:
diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py
index 7488bcc..ef3c00b 100644
--- a/tests/messages/test_frontend.py
+++ b/tests/messages/test_frontend.py
@@ -315,6 +315,37 @@ msgstr[1] ""
             actual_content = f.read()
         self.assertEqual(expected_content, actual_content)
 
+    def test_extraction_add_location_file(self):
+        self.dist.message_extractors = {
+            'project': [
+                ('**/ignored/**.*', 'ignore', None),
+                ('**.py', 'python', None),
+            ]
+        }
+        self.cmd.output_file = 'project/i18n/temp.pot'
+        self.cmd.add_location = 'file'
+        self.cmd.omit_header = True
+
+        self.cmd.finalize_options()
+        self.cmd.run()
+
+        self.assert_pot_file_exists()
+
+        expected_content = r"""#: project/file1.py
+msgid "bar"
+msgstr ""
+
+#: project/file2.py
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+        with open(self._pot_file(), 'U') as f:
+            actual_content = f.read()
+        self.assertEqual(expected_content, actual_content)
+
 
 class InitCatalogTestCase(unittest.TestCase):
 
@@ -1354,3 +1385,22 @@ def test_extract_cli_knows_dash_s():
     cmdinst = configure_cli_command("extract -s -o foo babel")
     assert isinstance(cmdinst, extract_messages)
     assert cmdinst.strip_comments
+
+
+def test_extract_add_location():
+    cmdinst = configure_cli_command("extract -o foo babel --add-location full")
+    assert isinstance(cmdinst, extract_messages)
+    assert cmdinst.add_location == 'full'
+    assert not cmdinst.no_location
+    assert cmdinst.include_lineno
+
+    cmdinst = configure_cli_command("extract -o foo babel --add-location file")
+    assert isinstance(cmdinst, extract_messages)
+    assert cmdinst.add_location == 'file'
+    assert not cmdinst.no_location
+    assert not cmdinst.include_lineno
+
+    cmdinst = configure_cli_command("extract -o foo babel --add-location never")
+    assert isinstance(cmdinst, extract_messages)
+    assert cmdinst.add_location == 'never'
+    assert cmdinst.no_location
diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py
index 4c525b5..d2a10dd 100644
--- a/tests/messages/test_pofile.py
+++ b/tests/messages/test_pofile.py
@@ -602,6 +602,26 @@ msgstr ""''')
         self.assertEqual(catalog['missing line number'].locations, [(u'broken_file.py', None)])
         self.assertEqual(catalog['broken line number'].locations, [])
 
+    def test_include_lineno(self):
+        catalog = Catalog()
+        catalog.add(u'foo', locations=[('main.py', 1)])
+        catalog.add(u'foo', locations=[('utils.py', 3)])
+        buf = BytesIO()
+        pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
+        self.assertEqual(b'''#: main.py:1 utils.py:3
+msgid "foo"
+msgstr ""''', buf.getvalue().strip())
+
+    def test_no_include_lineno(self):
+        catalog = Catalog()
+        catalog.add(u'foo', locations=[('main.py', 1)])
+        catalog.add(u'foo', locations=[('utils.py', 3)])
+        buf = BytesIO()
+        pofile.write_po(buf, catalog, omit_header=True, include_lineno=False)
+        self.assertEqual(b'''#: main.py utils.py
+msgid "foo"
+msgstr ""''', buf.getvalue().strip())
+
 
 class PofileFunctionsTestCase(unittest.TestCase):