pax_global_header00006660000000000000000000000064147012004710014506gustar00rootroot0000000000000052 comment=4f87817f5f33eb5d34c35d818684cdbb65a5a16b trubar-0.3.4/000077500000000000000000000000001470120047100130115ustar00rootroot00000000000000trubar-0.3.4/.github/000077500000000000000000000000001470120047100143515ustar00rootroot00000000000000trubar-0.3.4/.github/workflows/000077500000000000000000000000001470120047100164065ustar00rootroot00000000000000trubar-0.3.4/.github/workflows/pylint.yml000066400000000000000000000011151470120047100204460ustar00rootroot00000000000000name: Pylint on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install pylint python setup.py install - name: Analysing the code with pylint run: | pylint --rcfile pylintrc $(git ls-files '*.py') trubar-0.3.4/.github/workflows/test.yml000066400000000000000000000014041470120047100201070ustar00rootroot00000000000000name: Test on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: test-translations: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip setuptools python setup.py install - name: Run tests run: | export PYTHONDONTWRITEBYTECODE=1 python -m unittest discover trubar.tests -v cd trubar/tests ./test_command_line.shtrubar-0.3.4/.gitignore000066400000000000000000000000651470120047100150020ustar00rootroot00000000000000.coverage .idea/* dist/* __pycache__ trubar.egg-info trubar-0.3.4/CHANGELOG.md000066400000000000000000000047621470120047100146330ustar00rootroot00000000000000## 0.3.1 - 0.3.4 #### Bug fixes - Put auto imports after the docstring to avoid breaking doctests. - In multilingual mode, ensure that all variables from the original f-strings appear in the closure. - Fixed glitches in writing of translation files. - Set the default encoding to utf-8 rather than locale. ## 0.3 - 2024-06-13 - Support for switching between different languages (provisional, may change) - Drop support for Python 3.8. ## 0.2.5 - 2024-01-17 #### Bug fixes - Jaml reader crashed on empty files instead of reporting an error - Jaml write crashed on empty strings - Support (= test in CI) Python 3.11 and 3.12 ## 0.2.4 - 2023-03-30 #### New and improved functionality - Add option for in-place translation #### Bug fixes - After changed in 0.2.3, files without messages were inadvertently included in message files. This resulted in broken .jaml files. ## 0.2.2, 0.2.3 - 2023-03-11 #### New and improved functionality - (Compatibility breking change) Remove support for yaml-style (`|`) blocks in jaml. Use multiline (single-)quoted strings instead. - Arguments `-s` and `-d` are now required; trubar no longer falls back to current directory - If default configuration file is not found in current directory, Trubar also searches the directory with messages and source directory. `.trubarconfig` is now a primary default name. #### Bug fixes - `collect` with `--pattern` now keeps original messages from non-matching when updating an existing file - Replaces Windows-style backslashes with slashes in jaml keys #### Minor fixes - Fix message supression in `collect` ## 0.2.1 - 2023-01-13 - `collect` can now update existing files, reducing the need for `merge` - Minor reorganization of command line arguments ## 0.2 - 2023-01-08 #### New and improved functionality - A simplified proprietary variation of .jaml with round-trip comments and less need for quotes - New action `stat` - Different verbosity levels for `translate` action - Better error messages about malformed translations - New option `exclude-pattern` in config files instead of always skipping files whose names begin with "test_". - New argument `--static` instead of having a path for static files #### Bug fixes - Better testing before introducing the f-prefix: if the original already included braces, the f-prefix is not longer added - Fixed a bug occuring when paths ended with a trailing slash - Report an error when the source directory does not exist - Strings that are not translated are no longer reported as rejected in mergetrubar-0.3.4/LICENSE000066400000000000000000000021001470120047100140070ustar00rootroot00000000000000Copyright 2022, Janez Demšar, University of Ljubljana, Slovenia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.trubar-0.3.4/README.md000066400000000000000000000013471470120047100142750ustar00rootroot00000000000000## Trubar A tool for translation and localization of Python programs via modification of source files. Trubar supports f-strings and does not require any changes to the original source code, such as marking strings for translation. #### Installation and use Use pip to install Trubar ```sh pip install trubar` ``` Collect (or update) all strings in your project by ```sh trubar collect -s myproject/source messages.jaml ``` Add translations to messages.jaml and then run ```sh trubar translate -s myproject/source -d translated/myproject/source messages.jaml ``` to produce translated source files. See [Getting Started](http://janezd.github.io/trubar/getting-started) for a slightly longer introduction and complete documentation. trubar-0.3.4/docs/000077500000000000000000000000001470120047100137415ustar00rootroot00000000000000trubar-0.3.4/docs/code/000077500000000000000000000000001470120047100146535ustar00rootroot00000000000000trubar-0.3.4/docs/code/sample/000077500000000000000000000000001470120047100161345ustar00rootroot00000000000000trubar-0.3.4/docs/code/sample/__main__.py000066400000000000000000000005251470120047100202300ustar00rootroot00000000000000from farm.pigs import PigManager def main(): print("This program serves no useful purpose.") ns = input("Enter a number between 5 and 20: ") try: n = int(ns) except ValueError: print(f"'{n}' is not a number.") else: farm = PigManager(n) farm.walk() if __name__ == "__main__": main() trubar-0.3.4/docs/code/sample/farm/000077500000000000000000000000001470120047100170615ustar00rootroot00000000000000trubar-0.3.4/docs/code/sample/farm/__init__.py000066400000000000000000000000001470120047100211600ustar00rootroot00000000000000trubar-0.3.4/docs/code/sample/farm/pigs.py000066400000000000000000000004071470120047100203760ustar00rootroot00000000000000class PigManager: def __init__(self, n): if not 5 <= n <= 20: raise ValueError( f"Wrong number: {n} must be between 5 and 20.") self.n = n def walk(self): print(f"{self.n} little pigs went for a walk") trubar-0.3.4/docs/code/setup.py000066400000000000000000000000001470120047100163530ustar00rootroot00000000000000trubar-0.3.4/docs/command-line.md000066400000000000000000000167721470120047100166430ustar00rootroot00000000000000## Command line actions and their arguments Trubar is invoked by `trubar ` **Common arguments** are `-h` : Prints help and exits. `--conf ` : Specifies the [configuration file](../configuration). If not given, Trubar searches for `.trubarconfig.yaml` and `trubar-config.yaml` in current directory, directory with messages, and in source directory (for `collect` and `translate`). Action must be one of the following: - **collect:** collects strings from the specified source tree, - **translate:** copies the source tree and replaces strings with their translations, - **missing:** prepares a file that contains untranslated messages from another message file (i.e., those with `null` translations), - **merge:** inserts translations from one message file into another, - **template:** uses translations into one language to prepare a template for another, - **stat:** reports on the number of type of translations. ### Collect ``` trubar collect [-h] [-p pattern] [-r removed-translations] [-q] [-n] -s source-dir messages ``` Collects strings from the specified source tree, skipping files that don't end with `.py` or whose path includes `tests/test_`. (The latter can be changed in [configuration file](../configuration).) Strings with no effect are ignored; this is aimed at docstrings, but will also skip any other unused strings. If the output file already exists, it is updated: new messages are merged into it, existing translations are kept, and obsolete messages are removed. The latter can be recorded using the option `-r`. `messages` : The name of the file with messages (preferrably .jaml). If the file does not exist, it is created, otherwise it is updated with new messages and obsolete messages are removed. `-s `, `--source ` : Defines the root directory of the source tree. `-p `, `--pattern ` : Gives a pattern that the file path must include to be considered. The pattern is checked against the entire path; e.g. `-p rm/pi` would match the path `farm/pigs.py:`. `-u`, `--newer` : Only consider source files that are newer than the message file (if it exists). `-r `, `--removed ` : The name of the file for messages that were present in the messages file but no longer needed. If omitted, removed translations, if any, are saved to file `removed-from-`, where message is the name of the message file. If the file already exists `()` is appended to the name. `-n`, `--dry-run`: Run, but do not change the output file. The file with removed messages is still written. `-q`, `--quiet` : Supresses the output, except critical error messages. ### Translate ``` trubar translate [-h] [-p pattern] [--static static-files-dir] [-q] [-v {0,1,2,3}] [-n] -s source-dir -d destination-dir messages ``` Translates files with extension .py and writes them to destination directories, and copies all other files. Alternatively, `-i` can be given for translation in-place. Untranslated strings (marked `null`, `false` or `true`) are kept as they are. The action overwrites any existing files. `messages` : the name of the file with translated messages. `-s `, `--source ` : Root directory of the source tree. `-d `, `--dest ` : Destination directory. Either this option or `-i` is required. `-i`, `--inplace` : In-place translation. Either this or `-d` is required. `-p `, `--pattern ` : A pattern that the file path must include to be considered. `--static ` : Copies the file from the given path into destination tree; essentially `cp -R /`. This is used, for instance, for [adding modules with target-language related features](../localization/#plural-forms), like those for plural forms. This option can be given multiple times. If given, this argument overrides `static-files` from config file. `-q`, `--quiet` : Supresses output messages, except for critical. Overrides option `-v`. `-v `, `--verbosity ` : Sets the verbosity level to `0` (critical messages, equivalent to `-q`), `1` (report on files that are created or updated), `2` (also list files that have not changed) or `3` (report on all files, including those merely copied). This option is ignored in presence of `-q`. `-n`, `--dry-run` : Run, but do not write anything. ### Merge ``` trubar merge [-h] [-o output-file] [-u unused] [-p pattern] [-n] translations messages ``` Merges translations into message file. `translations` (required) : The "source" file with translations. `messages` (required) : File with messages into which the translations from `translations` are merged. This file is modified unless another output file is given. `-o `, `--output ` : The output file name; if omitted, the file given as `destination` is changed. `-u `, `--unused ` : A name of the file for messages that no longer appear in the sources. `-p `, `--pattern ` : A pattern that the file path must include to be considered. `-n`, `--dry-run` : Run, but do not write anything. ### Missing ``` trubar missing [-h] [-p pattern] [-m all-messages] -o output-file messages ``` Prepare a file with missing translations. A translation is missing if the translated message is `null`. Alternatively, the user can pass a file with all messages (option `-m`), and the translation is missing if the translated file either does not include it or has a `null` translation. `messages` (required) : The name of the file with messages. `-o `, `--output ` (required) : The name of the output file. `-m `, `--all-messages ` : If given, this file is considered to contain all messages. `-p `, `--pattern ` : If given, the output file will only contain messages from source files whose paths include the pattern. ### Template ``` trubar template [-h] [-p pattern] -o output-file messages ``` Create a template from existing translations. The output file will contain all strings that need attention. - Strings that are "translated" to `false` are skipped, because they must not be translated. - Strings that are "translated" to `true` are retained as they are. `true` indicates that they should probably be kept, but may also be translated if needed. - If string is translated, the original is kept, but translation is replaced by `null`. - Strings that are not translated (`null`) are kept. `messages` (required) : Existing (preferrably complete) translations into some language. `-o ` (required) : Output file name. `-p `, `--pattern ` : If given, the output file will only contain messages from source files whose paths include the pattern. ### Stat ``` trubar stat [-h] [-p pattern] messages ``` Print statistics about messages in the given file. Here's an example output. `messages` : File with messages. `-p `, `--pattern ` : If given, the count will include files whose paths include the pattern. ``` Total messages: 11161 Translated: 3257 29.2% Kept unchanged: 313 2.8% Programmatic: 7065 63.3% Total completed: 10635 95.3% Untranslated: 526 4.7% ``` Translated messages are those with actual translations, unchanged are translated as `true` and "programmatic" as `false`. "Untranslated" messages are those that are still marked as `null` and require translations. trubar-0.3.4/docs/configuration.md000066400000000000000000000035321470120047100171350ustar00rootroot00000000000000## Configuration file Configuration file is a yaml file with options-value pairs, for instance ``` smart-quotes: false auto-prefix: true auto-import: "from orangecanvas.utils.localization.si import plsi, plsi_sz" ``` If configuration is not specified, Truber looks for `.trubarconfig.yaml` and `trubar-config.yaml`,respectively, first in the current working directory and then in directory with message file, and then in source directory, as specified by `-s` argument (only for `collect` and `translate`). The available options are `smart-quotes` (default: true) : If set to `false`, strings in translated sources will have the same quotes as in the original source. Otherwise, if translation of a single-quoted includes a single quote, Trubar will output a double-quoted string and vice-versa. If translated message contains both types of quotes, they must be escaped with backslash. `auto-prefix` (default: true) : If set, Trubar will turn strings into f-strings if translation contains braces and adding an f- prefix makes it a syntactically valid string, *unless* the original string already included braces, in which case this may had been a pattern for `str.format`. `auto-import` (default: none) : A string that, if specified, is prepended to the beginning of each source file with translation. The use is described in the section on plural forms. `static-files` (default: none) : A path of directory, or a list of paths. whose content is copied into translated sources. See the section on plural forms. This option is overridden by `static` argument in the command line, if given. `exclude-pattern` (default: `"tests/test_"`) : A regular expression for filtering out the files that should not be translated. The primary use for this is to exclude unit tests. `encoding` (default: `"utf-8"`) : Characted encoding for .jaml files, such as `"utf-8"` or `"cp-1252"`. trubar-0.3.4/docs/extra.css000066400000000000000000000000351470120047100155740ustar00rootroot00000000000000pre { line-height: 1.6 } trubar-0.3.4/docs/getting-started.md000066400000000000000000000062261470120047100173760ustar00rootroot00000000000000## Getting Started Imagine a Python project named `sample` with the following typical structure. ``` sample farm __init__.py pigs.py __main__.py setup.py README LICENSE ``` File `sample/farm/pigs.py` contains ```python class PigManager: def __init__(self, n): if not 5 <= n <= 20: raise ValueError( f"Wrong number: number of pigs should be between 5 and 20, not {n}.") self.n = n def walk(self): print(f"{self.n} little pigs went for a walk") ``` and `sample/__main__.py` contains ```python from farm.pigs import PigManager def main(): ns = input("Enter a number between 5 and 20: ") try: n = int(ns) except ValueError: print(f"'{n}' is not a number") else: farm = PigManager(n) farm.walk() if __name__ == "__main__": main() ``` and `sample/farm/__init__.py` is empty. Note that, unlike in the gettext framework, messages are not "marked" for translation by being passed through a call to a translation function like `_`, `tr` or `gettext`. ### Collecting messages To collect all strings in the project, use [collect](../command-line/#collect). ``` trubar collect -s code/sample sample.jaml ``` The argument `-s` gives the root directory. This will usually not be the root of the project, which only contains "administrative" files like setup.py, but the directory with actual sources that need translation. The found strings are written into the output file, in our example `sample.jaml`. ```raw __main__.py: def `main`: 'Enter a number between 5 and 20: ': null "'{n}' is not a number.": null __main__: null farm/pigs.py: class `PigManager`: def `__init__`: 'Wrong number: number of pigs should be between 5 and 20, not {n}.': null def `walk`: {self.n} little pigs went for a walk: null ``` See the section about [Message files](../message-files) for details about the file format. ### Translating messages The next step is to edit the .jaml file: `null`'s need to be replaced by translations or marked in another way. Here's a Slovenian translation. ``` __main__.py: def `main`: 'Enter a number between 5 and 20: ': 'Vnesite število med 5 in 20: ' "'{n}' is not a number.": '{n}' ni število. __main__: false farm/pigs.py: class `PigManager`: def `__init__`: # I translated this, but I'm not sure it's needed. 'Wrong number: {n} is not between 5 and 20.': Napačno število: {n} ni med 5 in 20. def `walk`: {self.n} little pigs went for a walk: {self.n} prašičkov se je šlo sprehajat. ``` We translated `__main__` as `false`, which indicates that this string must not be translated. Other options are explained [later](../message-files/#translations). ### Applying translations In most scenarios, we first need to prepare a copy of the entire project, because Trubar will only copy the files within its scan range. Suppose that `../project_copy` contains such a copy. Now run [translate](../command-line/#translate). ``` trubar translate -s code/sample -d ../project_copy/code/sample sample.jaml ``` That's it.trubar-0.3.4/docs/index.md000066400000000000000000000005651470120047100154000ustar00rootroot00000000000000# Trubar A tool for translation and localization of Python programs via modification of source files. Trubar supports f-strings and does not require any changes to the original source code, such as marking strings for translation. See [Getting Started](getting-started) for a simple introduction. ## Installation Install trubar using `pip`. ``` pip install trubar ```trubar-0.3.4/docs/localization.md000066400000000000000000000136141470120047100167600ustar00rootroot00000000000000## F-strings and localization issues F-strings offer a powerful support for translation into language with complex grammar. ### Automated f-strings Trubar sometimes turn strings into f-string in translated files. If the original source contains an f-string, Trubar will keep the f-prefix in translated source even if the translation does not contain any parts to interpolate. Superfluous f-prefixes do not hurt. If the original string is not an f-string but the translation contains braces and prefixing this string with f- makes it a syntactically valid f-string, Trubar will add an f-prefix unless: - the original string already included braces (so this may be a pattern for `str.format`) - or this behaviour is explicitly disabled in [configuration](../configuration) by setting `auto-prefix: false`. ### Plural forms Trubar does not itself offer any specific functionality for plural forms. There is however a neat way of doing it, in particular if the original source is written in a translation-friendly way. The number of pigs in the getting started example is between 5 and 20, which requires a plural in any language with which the author of this text is sufficiently familiar. But now suppose that the number of pigs can be an arbitrary non-negative number. How do we translate `"{self.n} little pigs went for a walk"`? #### Plural forms in English The simplest way to support English plural in a large project would be to have a module, say in file `utils.localization.__init__.py`, with a function ```python def pl(n: int, forms: str) -> str: plural = int(n != 1) if "|" in forms: return forms.split("|")[plural] if forms[-1] in "yY" and forms[-2] not in "aeiouAEIOU": word = [forms, forms[:-1] + "ies"][plural] else: word = forms + "s" * plural if forms.isupper(): word = word.upper() return word ``` With this, the above string should be written as ```python "{self.n} little {pl(self.n, 'pig')} went for a walk" ``` The function take care of regular-ish plural forms, including `"piggy"` (`"piggies"`) as well as `"monkey"` (`"monkeys"`). If the plural is irregular, it requires both forms, separated by `|`, e.g. `pl(n, "leaf|leaves")`. #### Plural forms for other languages For other languages, one can write a similar module. If the project already includes `utils/localization/__init__.py`, an appropriate place for a Slovenian-language functions would be `utils/localization/si.py`. The function can be arbitrarily complex. This one takes care of Slovenian nouns, which have four plural forms in nominative and three in other cases, and also offers some automation for nominative case of some regular nouns. ```python def plsi(n: int, forms: str) -> str: n = abs(n) % 100 if n == 4: n = 3 elif n == 0 or n >= 5: n = 4 n -= 1 if "|" in forms: forms = forms.split("|") if n == 3 and len(forms) == 3: n -= 1 return forms[n] if forms[-1] == "a": return forms[:-1] + ("a", "i", "e", "")[n] else: return forms + ("", "a", "i", "ov")[n] ``` The translation of ```python {self.n} {pl(self.n, 'pig')} ``` into Slovenian is then ```python {self.n} {plsi(self.n, 'pujsek|pujska|pujski|pujskov')} ``` while the entire sentence ```python {self.n} little pigs went for a walk ``` requires changing most of the sentence ```python `"{self.n} {plsi(self.n, 'pujsek se je šel|pujska sta se šla|pujski so se šli|pujskov se je šlo')} sprehajat."` ``` Note that this works even if the original message does not contain any plural forms, for instance because the way it is phrased original is independent of the number. The only condition is that the number, in our case `self.n` is easily accessible in the string. This is also the reason why Trubar automatically turns strings into f-strings when it detects braces with expressions. #### Other localization functions The language-specific module can contain other support functions. For instance, the Slovenian translation of the word "with" in a message `"With {self.n} {pl(self.n, 'pigs')}"` is either "s" or "z", depending on the first sound of the number. Therefore, the Slovenian module for localization includes a function `plsi_sz(n)` that returns the necessary preposition for the given. The translation of the above would thus be ``` {plsi_sz(self.n)} {self.n} {pl(self.n, 'pujskom|pujskoma|pujski')} ``` The same mechanism can be used for other language quirks. #### Importing localization functions The above examples requires importing the localization functions, such as `plsi` and `plsi_sz`. First, the translated sources must include the necessary module, which does not exist in the original source. To this end, we need to prepare a directory with static files. In our case, we can have a directory named, for instance `si-local`, containing `si-local/utils/localization/__init__.py`. When translating, we instruct Trubar to copy this into translated source tree by adding an option `--static si-local` to the [`translate` action](../command-line/#translate). Second, all translated source files must include the necessary import. We do this using a directive in [configuration file](../configuration): ``` auto-import: "from orangecanvas.utils.localization.si import plsi, plsi_sz" ``` Trubar will prepend this line to the beginning of all files with any translations. #### Other forms of interpolation While Trubar works best with f-strings, other forms of interpolation in Python, `%` and `format` can sometimes be translated, provided the required data can be extracted. For instance, with `"%i little pigs" % self.n`, the translator would see the string part, `%i little pigs` and could translate it to `"{plsi(self.n, '%i pujsek|%i pujska|%i pujski|%i pujskov')}"`, that is, (s)he would replace the entire string with variations corresponding to different values of `self.n`. Persuading developpers to use f-strings is obviously a better alternative.trubar-0.3.4/docs/message-files.md000066400000000000000000000100371470120047100170100ustar00rootroot00000000000000# Message Files **TL;DR:** Look at the example from Getting Started. Read this page only if you run into problems and need details. Messages are stored in files with extension .jaml. Jaml is a simplified version of Yaml, limited to the functionality needed by Trubar. It does not support lists, flow collections, node anchors, different datatypes... This allows for simpler syntax, in particular less quotes. ### File Structure The first-level keys are file names, with their paths relative to the root passed in the `-s` option of `collect` and `translate`. (It is important to always use the same root for the project. Using `-s code/` wouldn't only include the strings from `setup.py`, but also prepend `sample` to the names of all scanned files!) Lower levels have keys that - start with `def` or `class`, and are followed by a subtree that starts in the next line, - or represents a potentially translatable string, followed by the translation in that same line (except when using [blocks](#blocks)). There is no indication about whether a string is an f-string or not, neither does it show what kind of quotes are used in the source, because none of this matters. Trubar also reads and writes standard yaml (it distinguishes between yaml and jaml by file extensions), but we don't recommend using it because their formatting is more complex and any comments written by translator are lost at reading. ### Translations Translator can treat a string in approximately three ways. - Provide a translation. - Mark with `false` to indicate that it *must not be* translated. An example of such string is `"__main__"` or `"rb"` when calling function `open`. - Mark it with `true`, if the strings that could be translated, but doesn't need it for this particular language or culture. A common example would be symbols like `"©️"`. - Leave it `null` until (s)he figures out what to do with it. The difference between `true` and `false` is important only when using this translation to [prepare templates](../scenarios/#preparing-templates) for translations into other languages. ### Comments Comments are useful to indicate questionable translations, brainstorm with other translators or oneself, and similar. Operations on message files, like extracting and merging, keep the comments at their respective places. A comment is always associated with some particular translation or entire function or class. It must be placed above it and conform to indendation. Comments cannot follow translations in the same line; a `#` symbol in translation is treated literally. ### Quotes - Translation must be quoted if it - begins or end with space, - begins with a quote (single or double) - it is (literally) `"false"`, `"true"` or `"null"`, (In addition, keys are quoted if they contain a colon followed by a space. But translator doesn't need to care because keys are provided by Trubar.) Single- and double-quoted strings are treated the same. The translation must begin and end with the same type of quote. The quotes used in message files are not related to the quotes used in source code. In the introductory example, all string in code use double quotes, while some strings in the message file are single-quoted and others double quoted, for convenience. A single-quoted string may contain double quotes and vice-versa; such quotes are treated literally. Any single (double) quotes within a single (double) quoted strings must be doubled, as in `'don''t forget to double the quotes.'`. ### Colons Colons in translations have no special meaning. Consider the following line from the example. ``` 'Wrong number: {n} is not between 5 and 20.': Napačno število: {n} ni med 5 in 20. ``` In standard yaml, the translation would need quotes because it includes a colon followed by space. In Jaml, this rule only applies to keys, which translator doesn't care about. Therefore: use colons at will. ### Multiline messages Strings can span over multiple lines. All whitespace in multiline strings is retained. JAML does not support any of the more complicated yaml syntax for multiline blocks.trubar-0.3.4/docs/questions.md000066400000000000000000000045211470120047100163170ustar00rootroot00000000000000## Questions ##### Why Truebar? It's not Truebar but Trubar. [Primož Trubar](https://en.wikipedia.org/wiki/Primo%C5%BE_Trubar) was the author of the first printed book in Slovenian language and was also the first to translated parts of the Bible into Slovene. If it's difficult to remember, imagine that the Tru- part stands for "translation utility", though this is only a coincidence. ##### And Jaml comes from ...? Shouldn't I, for once, include my name in something? :) ##### Why changing the sources? Why not calling a function, like in gettext? Because interpolation happens too early for that. Python's f-strings cannot be translated using gettext or similar tools, because gettexts translates a pattern, while f-strings are born interpolated. For instance, one could translate `"I see {n} little pigs".format(n=7)` because a string `"I see {n} little pigs"` can be passed to `gettext`, which returns a pattern in another language. In case of `f"I see {n} little pigs"`, the string `"I see {n} little pigs"` is never materialized. Syntactically, this is a `JoinedStr`, which is composed of a `Constant` `"I see "`, a `FormattedValue` (essentially `n`) and another `Constant`. At the moment when a `str` object is created and could be passed to `gettext`, the number `n` is already interpolated, hence `gettext` would receive a string like `"I see 7 little pigs"`. ##### Still, why not at least mark strings for translation in sources? First: why? You can either make it for translation in sources, or mark it for non-translation (`false`) in message files. Second: unless developpers are dedicated and disciplined, they will fail to mark strings, so somebody will have to mess with source later on. Third, it clutters the code. In `gettext`, the function that returns a translation is named `_`; this adds an underscore and parentheses (possibly within another parentheses...). In Python, `_` conventionally has a special meaning, so a longer name and more "visible" name would be required. We prefer keeping the code clean. ##### What if the same string appears twice in the same function/class/module, but needs different translations? Huh, find a neutral translation, or talk to developpers and ask them to split the function into two. Among 15 thousands messages in project Orange, this happenned ones and was resolvable by an (imperfect) neutral translation.trubar-0.3.4/docs/scenarios.md000066400000000000000000000023161470120047100162530ustar00rootroot00000000000000## Common Scenarios ### Translations maintenance As software changes, some messages may change or be removed, and new messages may appear. To update message file, re-run the collection, specifying the same output file. This will add new messages and keep the existing translations. Any messages that are no longer needed can be recorded in a separate file by pasing an option `-r`. ``` trubar collect -s code/sample -r removed.jaml sample.jaml ``` ### Preparing templates Unlike in our toy example, real projects contain a large proportion of string (one half up to two thirds, in our experience) that must not be translated, such as type annotations in form of strings, various string constants and arguments, old-style named tuple declarations and so forth. Deciding whether a partiuclar string needs translation or not requires looking into code and understanding it. This presents a huge burden for translator, but can, luckily, be done once for all languages. If a project is translated into one language, we can use ``` trubar template sample.jaml -o template.jaml ``` to prepare a template file `template.jaml` for other languages. The output file will contain all strings that need attention. See details below. trubar-0.3.4/mkdocs.yml000066400000000000000000000005721470120047100150200ustar00rootroot00000000000000site_name: Trubar theme: readthedocs markdown_extensions: - markdown.extensions.def_list nav: - Getting Started: getting-started.md - Message Files: message-files.md - Common Scenarios: scenarios.md - Command Line: command-line.md - Configuration: configuration.md - Localization Issues: localization.md - Questions: questions.md extra_css: - extra.css trubar-0.3.4/pylintrc000066400000000000000000000241471470120047100146100ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Add files or directories to the blacklist. They should be base names, not # paths. ignore= ignore-paths:.*/((shell_tests)|(test_module)|(test_module_2)|(code/sample))/.* # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins=pylint.extensions.eq_without_hash # Use multiple processes to speed up Pylint. jobs=0 # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" disable= missing-docstring, # I guess not no-else-return, no-else-raise, # else's may actually improve readability too-many-branches, # distracting rather than helpful duplicate-code, # I trust myself on this one cyclic-import, # weird false positive, can only disable globally too-many-positional-arguments # a new, not entirely useful check [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=colorized # Tells whether to display a full report or only the messages reports=no # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= [FORMAT] # Maximum number of characters on a single line. max-line-length=100 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=yes # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format=LF [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [SPELLING] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=yes [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=_$|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_,_cb,on_,_on_ [BASIC] # Good variable names which should always be accepted, separated by a comma good-names=ex,Run,_,a,c,d,e,i,j,k,m,n,o,p,t,u,v,w,x,y,A,X,Y,M # Bad variable names which should always be refused, separated by a comma bad-names= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Include a hint for the correct naming format with invalid-name include-naming-hint=yes # Regular expression matching correct method names # TODO: find a better way to allow long method names in test classes. method-rgx=[a-z_][a-zA-Z0-9_]{2,80}$ # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]{2,30}$ # Regular expression matching correct module names module-rgx=([a-z_][a-z0-9_]{2,30})$ # Regular expression matching correct class attribute names class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{2,30}$ # Regular expression matching correct constant names const-rgx=[A-Za-z_][A-Za-z0-9_]+$ # Regular expression matching correct function names function-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ # Regular expression matching correct variable names variable-rgx=[a-zA-Z_][a-zA-Z0-9_]{0,30}$ # Regular expression matching correct attribute names attr-rgx=[a-z_][a-zA-Z0-9_]*$ # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Regular expression matching correct argument names argument-rgx=[a-z_][a-zA-Z0-9_]*$ # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=7 [ELIF] # Maximum number of nested blocks for function / method body max-nested-blocks=5 [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignored-checks-for-mixins=no-member # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules=numpy,PyQt4,scipy # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). This supports can work # with qualified names. ignored-classes= # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO,HACK [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict,_fields,_replace,_source,_make [DESIGN] # Maximum number of arguments for function / method max-args=20 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=^_ # Maximum number of locals for function / method body max-locals=60 # Maximum number of return / yield for function / method body max-returns=20 # Maximum number of branch for function / method body max-branches=12 # Maximum number of statements in function / method body max-statements=200 # Maximum number of parents for a class (see R0901). max-parents=9 # Maximum number of attributes for a class (see R0902). max-attributes=20 # Minimum number of public methods for a class (see R0903). min-public-methods=0 # Maximum number of public methods for a class (see R0904). max-public-methods=40 # Maximum number of boolean expressions in a if statement max-bool-expr=10 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=optparse # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=builtins.Exception [isort] known-standard-library=pkg_resources trubar-0.3.4/pyproject.toml000066400000000000000000000001521470120047100157230ustar00rootroot00000000000000[build-system] requires = [ "setuptools >= 40.9.0", "wheel", ] build-backend = "setuptools.build_meta"trubar-0.3.4/requirements.txt000066400000000000000000000000151470120047100162710ustar00rootroot00000000000000libcst pyyamltrubar-0.3.4/setup.cfg000066400000000000000000000016441470120047100146370ustar00rootroot00000000000000[metadata] name = trubar version = 0.3.4 author = Janez Demsar author_email = janez.demsar@fri.uni-lj.si description = Utility for translation of Python sources long_description = A tool for localization of Python messages via translating source files. long_description_content_type = text/markdown classifiers = Development Status :: 2 - Pre-Alpha Intended Audience :: Developers Topic :: Software Development :: Localization License :: OSI Approved :: MIT License Programming Language :: Python :: 3 project_urls = Documentation = https://janezd.github.io/trubar Source code = https://github.com/janezd/trubar Bug tracker = https://github.com/janezd/trubar/issues [options] package_dir = = . packages = find: install_requires = libcst pyyaml python_requires = >=3.9 [options.extras_require] docs = mkdocs [options.entry_points] console_scripts = trubar = trubar.__main__:main trubar-0.3.4/setup.py000066400000000000000000000000461470120047100145230ustar00rootroot00000000000000from setuptools import setup setup() trubar-0.3.4/trubar/000077500000000000000000000000001470120047100143105ustar00rootroot00000000000000trubar-0.3.4/trubar/__init__.py000066400000000000000000000032631470120047100164250ustar00rootroot00000000000000import os from typing import Optional from trubar import actions def translate(msg_filename: str, source_dir: str, dest_dir: Optional[str] = None, config_file: Optional[str] = None, pattern="", verbosity=actions.ReportCritical, dry_run=False) -> None: """ Translate messages from source directory to destination directory. Args: msg_filename (str): name of file(s) with messages source_dir (str): source directory dest_dir (str, optional): target directory, or None for in-place translation config_file (str, optional): configuration file; contents override defaults pattern (str, optional): pattern for file selection verbosity (int, optional): verbosity level dry_run (bool, optional): if True, do not write any files """ # do not import at the top level to avoid re-exporting (and shadowing) config # pylint: disable=import-outside-toplevel from trubar.messages import load from trubar.utils import check_any_files from trubar.config import config if config_file: config.update_from_file(config_file) if config.languages: messages = [ load(os.path.join(config.base_dir, code, msg_filename)) if not settings.is_original else {} for code, settings in config.languages.items()] else: messages = [load(msg_filename)] trans_keys = set.union(*(set(trans) for trans in messages)) check_any_files(trans_keys, source_dir) actions.translate(messages, source_dir, dest_dir or source_dir, pattern, verbosity=verbosity, dry_run=dry_run) trubar-0.3.4/trubar/__main__.py000066400000000000000000000171021470120047100164030ustar00rootroot00000000000000import argparse import os import sys from trubar import translate from trubar.actions import \ collect, merge, missing, template, stat, \ ReportCritical from trubar.messages import load, dump from trubar.config import config from trubar.utils import check_any_files, dump_removed def check_dir_exists(path): if not os.path.isdir(path): if os.path.exists(path): print(f"{path} is not a directory.") else: print(f"Directory {path} does not exist.") sys.exit(2) def load_config(args): if args.conf: config.update_from_file(args.conf) return paths = [""] if getattr(args, "messages", False): paths.append(os.path.split(args.messages)[0]) if getattr(args, "source", False): paths.append(args.source) for path in paths: for name in (".trubarconfig.yaml", "trubar-config.yaml"): fullname = os.path.join(path, name) if os.path.exists(fullname): config.update_from_file(fullname) return def main() -> None: def add_parser(name, desc): subparser = subparsers.add_parser(name, help=desc, description=desc) subparser.add_argument( "-p", "--pattern", default="", metavar="pattern", help="include only files whose full path include the pattern") return subparser argparser = argparse.ArgumentParser() argparser.add_argument( "--conf", default="", metavar="configuration-file", help="configuration file") subparsers = argparser.add_subparsers(required=True, dest="action") parser = add_parser("collect", "Collect message strings in source files") parser.add_argument( "-s", "--source", metavar="source-dir", required=True, help="source path") parser.add_argument( "-u", "--newer", action="store_true", help="check only source files that are newer than the message file") parser.add_argument( "messages", metavar="messages", help="existing or new file with messages") parser.add_argument( "-r", "--removed", metavar="removed-translations", default=None, help="file for removed translations, if any") parser.add_argument( "-q", "--quiet", action="store_true", help="supress intermediary outputs") parser.add_argument( "-n", "--dry-run", action="store_true", help="don't write the output file; removed translations (if any) are written" ) parser = add_parser("translate", "Prepare sources with translations") parser.add_argument( "messages", metavar="messages", help="file with translated messages") parser.add_argument( "-d", "--dest", metavar="destination-dir", help="destination path") parser.add_argument( "-i", "--inplace", action="store_true", help="translate files in-place") parser.add_argument( "-s", "--source", metavar="source-dir", required=True, help="source path") parser.add_argument( "--static", metavar="static-files-dir", action="append", help="directory(-ies) with static files to copy") parser.add_argument( "-q", "--quiet", action="store_true", help="supress intermediary outputs") parser.add_argument( "-v", "--verbosity", type=int, choices=range(4), default=1, help="verbosity (0=quiet, 1=updates, 2=translations, 3=all") parser.add_argument( "-n", "--dry-run", action="store_true", help="don't write anything; perform a trial run to check the structure" ) parser = add_parser("merge", "Merge translations into template or existing " "translations") parser.add_argument( "translations", metavar="translations", help="new or updated translations") parser.add_argument( "messages", metavar="messages", help="file with messages; this file is updated unless --output is given") parser.add_argument( "-o", "--output", metavar="output-file", help="output file; if omitted, existing file will updated") parser.add_argument( "-u", "--unused", metavar="unused", default=None, help="file for unused translations (if any)") parser.add_argument( "-n", "--dry-run", action="store_true", help="run without writing anything" ) parser = add_parser("template", "Create empty template from existing translations") parser.add_argument( "messages", metavar="translations", help="file with existing translations for another language") parser.add_argument( "-o", "--output", metavar="output-file", required=True, help="output file") parser = add_parser("missing", "Prepare a file with missing translations") parser.add_argument( "messages", metavar="messages", help="file with existing translations") parser.add_argument( "-m", "--all-messages", metavar="all-messages", required=False, help="all messages") parser.add_argument( "-o", "--output", metavar="output-file", required=True, help="missing translations") parser = add_parser("stat", "Show statistics about messages in the file") parser.add_argument( "messages", metavar="messages", help="file with messages") args = argparser.parse_args(sys.argv[1:]) load_config(args) pattern = args.pattern if args.action == "collect": min_time = None check_dir_exists(args.source) if os.path.exists(args.messages): existing = load(args.messages) check_any_files(set(existing), args.source) if args.newer: min_time = os.stat(args.messages).st_mtime else: existing = {} messages, removed = collect(args.source, existing, pattern, quiet=args.quiet, min_time=min_time) if not args.dry_run: dump(messages, args.messages) dump_removed(removed, args.removed, args.messages) elif args.action == "translate": check_dir_exists(args.source) if args.inplace: if args.dest: argparser.error("options -d and -i are incompatible") else: args.dest = args.source elif not args.dest: argparser.error("specify destination (-d) or translate in place (-i)") if args.static: config.set_static_files(args.static) verbosity = ReportCritical if args.quiet else args.verbosity translate(args.messages, args.source, args.dest, pattern=pattern, verbosity=verbosity, dry_run=args.dry_run) elif args.action == "merge": additional = load(args.translations) existing = load(args.messages) unused = merge(additional, existing, pattern, print_unused=not args.unused) if not args.dry_run: dump(existing, args.output or args.messages) if args.unused and unused: dump(unused, args.unused) elif args.action == "template": existing = load(args.messages) new = template(existing, pattern) dump(new, args.output) elif args.action == "missing": translations = load(args.messages) messages = load(args.all_messages) \ if args.all_messages else translations needed = missing(translations, messages, pattern) dump(needed, args.output) elif args.action == "stat": messages = load(args.messages) stat(messages, pattern) if __name__ == "__main__": main() trubar-0.3.4/trubar/actions.py000066400000000000000000000666521470120047100163410ustar00rootroot00000000000000import dataclasses import os import re import shutil import json from itertools import islice from typing import Union, List, Optional, NamedTuple, Tuple, Dict import libcst as cst from libcst.metadata import ParentNodeProvider from trubar.utils import walk_files, make_list from trubar.messages import MsgNode, MsgDict from trubar.config import config __all__ = ["collect", "translate", "merge", "missing", "template", "ReportCritical", "ReportUpdates", "ReportTranslations", "ReportAll"] NamespaceNode = Union[cst.Module, cst.FunctionDef, cst.ClassDef] SomeString = Union[cst.SimpleString, cst.FormattedString] re_single_quote = re.compile(r"(^|[^\\])'") re_double_quote = re.compile(r'(^|[^\\])"') re_braced = re.compile(r"{.+}") all_quotes = ("'", '"', "'''", '"""') class State(NamedTuple): node: NamespaceNode name: str def prefix_for_node(node: NamespaceNode): if isinstance(node, cst.FunctionDef): return "def " elif isinstance(node, cst.ClassDef): return "class " assert isinstance(node, cst.Module) return "" class StringCollector(cst.CSTVisitor): METADATA_DEPENDENCIES = (ParentNodeProvider, ) @classmethod def parse_file(cls, fullname: str): collector = cls() with open(fullname, encoding=config.encoding) as f: tree = cst.metadata.MetadataWrapper(cst.parse_module(f.read())) tree.visit(collector) return MsgNode(collector.contexts[0]) def __init__(self): super().__init__() self.module: Optional[cst.Module] = None self.module_name: Optional[str] = None # The stack of nodes (module, class, function) corresponding to # the element of stack of contexts self.function_stack: List[State] = [] self.contexts: List[MsgDict] = [] def visit_Module(self, node: cst.Module) -> bool: self.module = node self.push_context(node, "") return True def push_context(self, node: NamespaceNode, name: Optional[str] = None) -> None: if name is None: name = f"{prefix_for_node(node)}`{node.name.value}`" self.function_stack.append(State(node, name)) self.contexts.append({}) def pop_context(self) -> None: state = self.function_stack.pop() context = self.contexts.pop() if context: self.contexts[-1][state.name] = MsgNode(context) def is_useless_string(self, node: cst.CSTNode) -> bool: # This is primarily to exclude docstrings: exclude strings if they # represent the entire body of a simple statement. # It will not exclude, e.g. line `"a" + "b"`. parent = self.get_metadata(ParentNodeProvider, node) grand = self.get_metadata(ParentNodeProvider, parent) return isinstance(parent, cst.Expr) \ and isinstance(grand, cst.SimpleStatementLine) \ and len(grand.body) == 1 def visit_ClassDef(self, node: cst.ClassDef) -> bool: self.push_context(node) return True def leave_ClassDef(self, _) -> None: self.pop_context() def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: self.push_context(node) return True def leave_FunctionDef(self, _) -> None: self.pop_context() def visit_FormattedString( self, node: cst.FormattedString) -> bool: lq = len(node.quote) if not self.is_useless_string(node): text = self.module.code_for_node(node)[len(node.prefix) + lq:-lq] self.contexts[-1][text] = MsgNode(None) return False # don't visit anything within an f-string! def visit_SimpleString(self, node: cst.SimpleString) -> bool: lq = len(node.quote) s = self.module.code_for_node(node)[len(node.prefix) + lq:-lq] if s and not self.is_useless_string(node): self.contexts[-1][s] = MsgNode(None) return False # doesn't matter, there's nothing down there anyway class TranslationError(Exception): pass class CountImportsFromFuture(cst.CSTVisitor): def __init__(self): super().__init__() self.count = 0 self.has_docstring = False def visit_Module(self, node: cst.Module): self.has_docstring = node.get_docstring() def visit_ImportFrom(self, node: cst.ImportFrom): if node.module is not None and node.module.value == "__future__": self.count += 1 return False class StringTranslatorBase(cst.CSTTransformer): METADATA_DEPENDENCIES = (ParentNodeProvider, ) def __init__(self, module: cst.Module, auto_import: Optional[cst.CSTNode] = None, n_future_imports: Optional[int] = 0, has_docstring: bool = False): super().__init__() self.module = module self.context_stack: Union[List[MsgDict], List[List[MsgDict]]] = [] self.auto_import = auto_import self.auto_import_after = n_future_imports or None self.import_after_docstring = has_docstring and not n_future_imports @property def context(self): return self.context_stack[-1] def push_context(self, node: NamespaceNode) -> None: raise NotImplementedError def pop_context(self) -> None: self.context_stack.pop() def __leave(self, _, updated_node: cst.CSTNode) -> cst.CSTNode: self.pop_context() return updated_node def translate( self, node: SomeString, updated_node: SomeString) -> cst.CSTNode: raise NotImplementedError leave_ClassDef = __leave leave_FunctionDef = __leave def leave_SimpleStatementLine(self, _, updated_node: cst.CSTNode ) -> cst.CSTNode: # Decrease the counter of __future__imports, and put auto imports # after the node when the counter reaches zero if self.auto_import is None: return updated_node if self.auto_import_after is not None: self.auto_import_after -= sum( isinstance(child, cst.ImportFrom) and child.module.value == "__future__" for child in updated_node.body) if self.import_after_docstring: import_now = isinstance(updated_node, cst.SimpleStatementLine) else: import_now = self.auto_import_after == 0 if import_now: updated_node = cst.FlattenSentinel([updated_node, *self.auto_import]) self.auto_import = None return updated_node def on_leave(self, original_node: cst.CSTNodeT, updated_node: cst.CSTNodeT) -> cst.CSTNode: # If there are no imports from __future__ insert auto import # before the first node updated_node = super().on_leave(original_node, updated_node) if self.auto_import \ and not self.import_after_docstring \ and self.auto_import_after is None \ and not isinstance(original_node, cst.Module) \ and isinstance( self.get_metadata(ParentNodeProvider, original_node), cst.Module): updated_node = cst.FlattenSentinel([*self.auto_import, updated_node]) self.auto_import = None return updated_node def visit_ClassDef(self, node: NamespaceNode) -> None: self.push_context(node) def visit_FunctionDef(self, node: NamespaceNode) -> None: self.push_context(node) def leave_FormattedString( self, original_node: cst.FormattedString, updated_node: cst.FormattedString) -> cst.CSTNode: return self.translate(original_node, updated_node) def leave_SimpleString( self, original_node: cst.SimpleString, updated_node: cst.SimpleString) -> cst.CSTNode: return self.translate(original_node, updated_node) def visit_ConcatenatedString( self, _) -> bool: return False def leave_ConcatenatedString( self, original_node: cst.ConcatenatedString, updated_node: cst.ConcatenatedString) -> cst.CSTNode: def compose_concatenation(node: cst.ConcatenatedString): left = self.translate(node.left, node.left) if isinstance(node.right, cst.ConcatenatedString): right = compose_concatenation(node.right) else: right = self.translate(node.right, node.right) return cst.BinaryOperation( left, cst.Add(), right, (cst.LeftParen(),), (cst.RightParen(), )) return compose_concatenation(updated_node) class StringTranslator(StringTranslatorBase): def __init__(self, context: MsgDict, module: cst.Module, auto_import: Optional[cst.CSTNode] = None, n_future_imports: Optional[int] = None, has_docstring: bool = False): super().__init__(module, auto_import, n_future_imports, has_docstring) self.context_stack = [context] def push_context(self, node: NamespaceNode) -> None: key = f"{prefix_for_node(node)}`{node.name.value}`" space = self.context[key].value if key in self.context else {} self.context_stack.append(space) def translate( self, node: SomeString, updated_node: SomeString) -> cst.CSTNode: if not self.context: return updated_node lq = len(node.quote) original = self.module.code_for_node(node)[len(node.prefix) + lq:-lq] if original not in self.context: return updated_node translation = self.context[original].value if translation in (None, False, True): return updated_node assert isinstance(translation, str) quote = node.quote if config.smart_quotes: has_single = re_single_quote.search(translation) has_double = re_double_quote.search(translation) if quote == "'" and has_single and not has_double: quote = '"' elif quote == '"' and has_double and not has_single: quote = "'" if config.auto_prefix \ and "f" not in node.prefix and not re_braced.search(original) \ and re_braced.search(translation) : try: new_node = cst.parse_expression( f'f{node.prefix}{quote}{translation}{quote}') except cst.ParserSyntaxError: pass else: assert isinstance(new_node, cst.FormattedString) if any(isinstance(part, cst.FormattedStringExpression) for part in new_node.parts): return new_node try: new_node = cst.parse_expression( f'{node.prefix}{quote}{translation}{quote}') except cst.ParserSyntaxError: if "\n" in translation and len(quote) != 3: unescaped = " Unescaped \\n?" else: unescaped = "" raise TranslationError( f'\nProbable syntax error in translation.{unescaped}\n' f'Original: {original}\n' f'Translation: {translation}') from None return new_node class StringTranslatorMultilingual(StringTranslatorBase): def __init__(self, contexts: List[MsgDict], message_tables: List[List[str]], module: cst.Module, auto_import: Optional[cst.CSTNode] = None, n_future_imports: Optional[int] = None, has_docstring: bool = False): super().__init__(module, auto_import, n_future_imports, has_docstring) self.context_stack = [contexts] self.message_tables = message_tables def push_context(self, node: NamespaceNode) -> None: key = f"{prefix_for_node(node)}`{node.name.value}`" space = [lang_context[key].value if key in lang_context else {} for lang_context in self.context] self.context_stack.append(space) @classmethod def _f_string_languages(cls, node: SomeString, messages: List[str]) -> List[str]: # Don't prefix if auto_prefix is off, or we already have it, # or the original already has braces (although without the f-prefix) if not config.auto_prefix \ or "f" in node.prefix \ or re_braced.search(messages[0]): return [] quotes = (node.quote, ) + (all_quotes if config.smart_quotes else ()) add_f = [] for translation, langdef in zip(messages[1:], islice(config.languages.values(), 1, None)): if not re_braced.search(translation): continue for quote in quotes: try: new_node = cst.parse_expression( f'f{node.prefix}{quote}{translation}{quote}') assert isinstance(new_node, cst.FormattedString) except cst.ParserSyntaxError: continue if any(isinstance(part, cst.FormattedStringExpression) for part in new_node.parts): add_f.append(f"{langdef.international_name} ({translation})") break return add_f @staticmethod def _get_quote(node: SomeString, orig_str: str, messages: List[str], prefix: str, need_f: List[str]) -> str: quotes = (node.quote, ) + (all_quotes if config.smart_quotes else ()) for fquote in quotes: for translation in messages: try: compile(f"{prefix}{fquote}{translation}{fquote}", '', 'eval') except SyntaxError: break else: return fquote # No suitable quotes, raise an exception hints = "" if "f" in node.prefix: hints += f"\n- String {orig_str} is an f-string" else: hints += ( f"\n- Original string, {orig_str}, is not an f-string, " f"but {make_list(need_f, 'seem')} to require f-strings " "and auto-prefix option is set.") if config.smart_quotes: hints += \ "\n- I tried all quote types, even triple-quotes" else: hints += \ "\n- Try enabling smart quotes to allow changing the quote type" if any(map(re_single_quote.search, messages)) \ and any(map(re_double_quote.search, messages)): hints += \ "\n- Some translations use single quotes and some use double" if len(fquote) != 3 and "\n" in "".join(messages[1:]): hints += \ "\n- Check for any unescaped \\n's" languages = iter(config.languages.values()) original = f"{orig_str} ({next(languages).international_name})" trans = "\n".join(f" - {msg} ({langdef.international_name})" for msg, langdef in zip(messages[1:], languages)) raise TranslationError( f"Probable syntax error in translation of {orig_str}.\n" f"Original: {original}\n" f"Translations:\n{trans}\n" "Some hints:" + hints) def translate( self, node: SomeString, updated_node: SomeString) -> cst.CSTNode: if not self.context: return updated_node lq = len(node.quote) orig_str = self.module.code_for_node(node) original = orig_str[len(node.prefix) + lq:-lq] messages = [lang_context[original].value if original in lang_context else None for lang_context in self.context] assert all(isinstance(translation, (str, bool, type(None))) for translation in messages) if all(message in (None, False, True) for message in messages): return updated_node messages = [ translation if isinstance(translation, str) else original for translation in messages] need_f = self._f_string_languages(node, messages) prefix = "f" + node.prefix if need_f else node.prefix idx = len(self.message_tables[0]) if "f" in prefix: quote = self._get_quote(node, orig_str, messages, prefix, need_f) for message, table in zip(messages, self.message_tables): table.append(f"{prefix}{quote}{message}{quote}") trans = f'_tr.e(_tr.c({idx}, {orig_str}))' else: for message, table in zip(messages, self.message_tables): table.append(message .encode('latin-1', 'backslashreplace') .decode('unicode-escape')) trans = f"_tr.m[{idx}, {orig_str}]" return cst.parse_expression(trans) def collect(source: str, existing: Optional[MsgDict] = None, pattern: str = "", *, quiet=False, min_time=None) -> Tuple[MsgDict, MsgDict]: messages = {} removed = {} # No pattern when calling walk_files: we must get all files so that # existing messages in skipped files are kept. We check the pattern here. for name, fullname in walk_files(source, "", select=True): if pattern in name and ( min_time is None or os.stat(fullname).st_mtime >= min_time): if not quiet: print(f"Parsing {name}") collected = StringCollector.parse_file(fullname) if collected.value: messages[name] = collected if name in existing: removals = MsgNode(merge( existing.pop(name).value, collected.value, "", name, print_unused=False)) if removals.value: removed[name] = removals elif name in existing: messages[name] = existing.pop(name) existing = {name: trans for name, trans in existing.items() if _any_translations(trans.value)} removed.update(existing) return messages, removed ReportCritical, ReportUpdates, ReportTranslations, ReportAll = range(4) def translate(translations: Dict[str, MsgDict], source: str, destination: str, pattern: str, *, verbosity=ReportUpdates, dry_run=False) -> None: def write_if_different(data, dest): try: with open(dest, encoding=config.encoding) as f: diff = 1 if f.read() != data else 0 except OSError: diff = 2 if diff and not dry_run: with open(dest, "wt", encoding=config.encoding) as f: f.write(data) return diff def copy_if_different(src, dest): with open(src, encoding=config.encoding) as f: return write_if_different(f.read(), dest) inplace = os.path.realpath(source) == os.path.realpath(destination) if dry_run or inplace: def noop(*_, **_1): pass copyfile = copytree = makedirs = noop else: copyfile, copytree = shutil.copyfile, shutil.copytree makedirs = os.makedirs any_reports = False def report(s, level): nonlocal any_reports if level <= verbosity: any_reports = True print(s) if config.auto_import: imports = cst.parse_module("\n".join(config.auto_import)) auto_import = imports.body else: auto_import = None if config.languages: message_tables = [[language.name, language.international_name] for language in config.languages.values()] else: message_tables = None for name, fullname in walk_files(source, pattern, select=False): transname = os.path.join(destination, name) path, _ = os.path.split(transname) makedirs(path, exist_ok=True) # Copy anything that is not Python if not name.endswith(".py") \ or config.exclude_re and config.exclude_re.search(fullname): copyfile(fullname, transname) continue # Copy files without translations if not any(name in trans and _any_translations(trans[name].value) for trans in translations): if inplace: continue diff = copy_if_different(fullname, transname) if diff: report(f"Copying {name} (no translations)", ReportAll) else: report(f"Skipping {name} (unchanged; no translations)", ReportAll) continue # Parse original sources try: with open(fullname, encoding=config.encoding) as f: orig_source = f.read() tree = cst.parse_module(orig_source) except Exception: print(f"Error when parsing {name}") raise if auto_import is not None: counter = CountImportsFromFuture() tree.visit(counter) n_future_imports = counter.count has_docstring = counter.has_docstring else: n_future_imports = None has_docstring = None # Replace with translations, produce new sources try: trans_name = [ trans[name].value if name in trans else {} for trans in translations ] if config.languages is None: translator = StringTranslator( trans_name[0], tree, auto_import, n_future_imports, has_docstring) else: translator = StringTranslatorMultilingual( trans_name, message_tables, tree, auto_import, n_future_imports, has_docstring) tree = cst.metadata.MetadataWrapper(tree) translated = tree.visit(translator) trans_source = tree.module.code_for_node(translated) except Exception: print(f"Error when inserting translations into {name}") raise diff = write_if_different(trans_source, transname) if diff == 0: report(f"Skipping {name} (unchanged)", ReportTranslations) elif diff == 1: report(f"Updating translated {name}", ReportUpdates) else: # diff == 2 report(f"Creating translated {name}", ReportUpdates) if not dry_run: for path in config.static_files: report(f"Copying files from '{path}'", ReportAll) copytree(path, destination, dirs_exist_ok=True) if not any_reports and verbosity > ReportCritical: print("No changes.") if config.languages: i18ndir = os.path.join(destination, "i18n") os.makedirs(i18ndir, exist_ok=True) for langdef, messages in zip(config.languages.values(), message_tables): fname = os.path.join(i18ndir, f"{langdef.international_name}.json") with open(fname, "wt", encoding=config.encoding) as f: json.dump(messages, f) def _any_translations(translations: MsgDict): return any(isinstance(value, str) or isinstance(value, dict) and _any_translations(value) for value in (msg.value for msg in translations.values())) def missing(translations: MsgDict, messages: MsgDict, pattern: str = "") -> MsgDict: no_translations: MsgDict = {} for obj, orig in messages.items(): if pattern not in obj: continue trans = translations.get(obj) if trans is None: no_translations[obj] = orig # orig may be `None` or a whole subdict elif isinstance(orig.value, dict): if submiss := missing(translations[obj].value, orig.value, ""): no_translations[obj] = MsgNode(submiss, trans.comments or orig.comments) elif trans.value is None: no_translations[obj] = trans # this keeps comments return no_translations def merge(additional: MsgDict, existing: MsgDict, pattern: str = "", path: str = "", print_unused=True) -> MsgDict: unused: MsgDict = {} for msg, trans in additional.items(): if pattern not in msg: continue npath = path + "/" * bool(path) + msg if msg not in existing: if trans.value and (not isinstance(trans.value, dict) or _any_translations(trans.value)): if print_unused: print(f"{npath} not in target structure") unused[msg] = trans elif isinstance(trans.value, dict): subreject = merge(trans.value, existing[msg].value, "", npath, print_unused=print_unused) if subreject: unused[msg] = MsgNode(subreject) elif trans.value is not None: existing[msg] = trans return unused def template(existing: MsgDict, pattern: str = "") -> MsgDict: new_template: MsgDict = {} for msg, trans in existing.items(): if pattern not in msg: continue if isinstance(trans.value, dict): if subtemplate := template(trans.value): new_template[msg] = MsgNode(subtemplate, trans.comments) elif trans.value is not False: new_template[msg] = MsgNode(None, trans.comments) return new_template @dataclasses.dataclass class Stat: translated: int = 0 kept: int = 0 untranslated: int = 0 programmatic: int = 0 def __add__(self, other): return Stat(self.translated + other.translated, self.kept + other.kept, self.untranslated + other.untranslated, self.programmatic + other.programmatic) def __abs__(self): return self.translated + self.kept \ + self.untranslated + self.programmatic @classmethod def collect_stat(cls, messages): values = [obj.value for obj in messages.values()] return sum((cls.collect_stat(value) for value in values if isinstance(value, dict)), start=Stat(sum(isinstance(val, str) for val in values), values.count(True), values.count(None), values.count(False))) def stat(messages: MsgDict, pattern: str): if pattern: messages = {k: v for k, v in messages.items() if pattern in k} stats = Stat.collect_stat(messages) n_all = abs(stats) if not n_all: print("No messages") else: print(f"Total messages: {abs(stats)}") print() print(f"{'Translated:':16}" f"{stats.translated:6}{100 * stats.translated / n_all:8.1f}%") print(f"{'Kept unchanged:':16}" f"{stats.kept:6}{100 * stats.kept / n_all:8.1f}%") print(f"{'Programmatic:':16}" f"{stats.programmatic:6}{100 * stats.programmatic / n_all:8.1f}%") print(f"{'Total completed:':16}" f"{n_all - stats.untranslated:6}" f"{100 - 100 * stats.untranslated / n_all:8.1f}%") print() print(f"{'Untranslated:':16}" f"{stats.untranslated:6}{100 * stats.untranslated / n_all:8.1f}%") trubar-0.3.4/trubar/config.py000066400000000000000000000120071470120047100161270ustar00rootroot00000000000000import sys import os import re import dataclasses from typing import Optional import yaml @dataclasses.dataclass class LanguageDef: name: str international_name: str is_original: bool @dataclasses.dataclass class Configuration: base_dir: Optional[str] = None smart_quotes: bool = True auto_prefix: bool = True auto_import: tuple = () static_files: tuple = () exclude_pattern: str = "tests/test_" encoding: str = "utf-8" languages = None def __post_init__(self): self.__update_exclude_re() def update_from_file(self, filename): if not os.path.exists(filename): print(f"Can't open configuration file {filename}") sys.exit(4) try: with open(filename, encoding=self.encoding) as f: settings = yaml.load(f, Loader=yaml.Loader) except yaml.YAMLError as exc: print(f"Invalid configuration file: {exc}") sys.exit(4) self.base_dir, _ = os.path.split(filename) fieldict = {field.name: field for field in dataclasses.fields(self)} for name, value in settings.items(): if name == "languages": self.parse_languages(value) continue name = name.replace("-", "_") field = fieldict.get(name, None) if field is None: print(f"Unrecognized configuration setting: {name}") sys.exit(4) if field.type is bool and value not in (True, False): print(f"Invalid value for '{name}': {value}") sys.exit(4) else: try: if value is None: value = field.type() elif field.type is tuple and isinstance(value, str): value = (value, ) else: value = field.type(value) except ValueError: print(f"Invalid value for '{name}': {value}") sys.exit(4) if field.type is tuple and hasattr(self, name): setattr(self, name, getattr(self, name) + value) else: setattr(self, name, value) self.__update_exclude_re() if isinstance(self.static_files, str): self.static_files = (self.static_files, ) self.__check_static_files() def parse_languages(self, value): language_options = {"name", "original", "international-name", "auto-import"} self.languages = {} for code, values in value.items(): if "name" not in values: print(f"Language '{code}' is missing a 'name' option") sys.exit(4) name = values["name"] international_name = values.get("international-name", name) # Use a list, not set to keep the original order unknown = [opt for opt in values if opt not in language_options] if unknown: print(f"Unknown options for language '{code}': " + ', '.join(unknown)) sys.exit(4) is_original = values.get("original", False) lang_dir = os.path.join(self.base_dir, code) if not (is_original or os.path.exists(lang_dir)): print(f"Directory for language '{code}' is missing " f"({lang_dir}).") sys.exit(4) self.languages[code] = LanguageDef( name=name, international_name=international_name, is_original=is_original ) if "auto-import" in values: self.auto_import = self.auto_import + (values["auto-import"], ) static_dir = os.path.join(lang_dir, "static") if os.path.exists(static_dir): self.static_files = self.static_files + (static_dir, ) sorted_langs = sorted(self.languages.items(), key=lambda item: not item[1].is_original) if not sorted_langs[0][1].is_original: print("Original language is not defined") sys.exit(4) self.languages = dict(sorted_langs) def set_static_files(self, static): self.static_files = self.static_files + tuple(static) self.__check_static_files() def set_exclude_pattern(self, pattern): self.exclude_pattern = pattern self.__update_exclude_re() def __update_exclude_re(self): # This function is called from (post)init # pylint: disable=attribute-defined-outside-init self.exclude_pattern = self.exclude_pattern.strip() if self.exclude_pattern: self.exclude_re = re.compile(self.exclude_pattern) else: self.exclude_re = None def __check_static_files(self): for path in self.static_files: if not os.path.exists(path): print(f"Static files path '{path}' does not exist") sys.exit(4) config = Configuration() trubar-0.3.4/trubar/jaml.py000066400000000000000000000111231470120047100156030ustar00rootroot00000000000000import re class JamlError(Exception): pass def readfile(name, encoding="utf8"): with open(name, encoding=encoding) as f: return read(f.read()) def read(text): return readlines(text.splitlines()) def readlines(lines): # prevent circular import, pylint: disable=import-outside-toplevel from trubar.messages import MsgNode def error(msg, line=None): raise JamlError(f"Line {line or lineno}: {msg}") from None def read_quoted(line): nonlocal lineno start_line = lineno block = "" q, line = line[0], line[1:] while (mo := re.match(f"((?:[^{q}]|(?:{q}{q}))*){q}(?!{q})(.*)", line) ) is None: block += line + "\n" try: lineno, line = next(linegen) except StopIteration: error("file ends before the end of quoted string", start_line) inside, after = mo.groups() block += inside return block.replace(2 * q, q), after def check_no_comments(): if comments: error("stray comment", comment_start) items = {} stack = [(-1, items, True)] comments = [] comment_start = None linegen = enumerate(lines, start=1) lineno, line = 0, "" # for error reporting for empty files for lineno, line in linegen: # Skip empty lines if not line.strip(): continue sline = line.lstrip() indent = len(line) - len(sline) line = sline # Indentation last_indent, _, indent_expected = stack[-1] if indent_expected: if indent <= last_indent: error("indent expected") stack.append((indent, stack.pop()[1], False)) elif indent > last_indent: error("unexpected indent") elif indent < last_indent: check_no_comments() while stack and indent != stack[-1][0]: stack.pop() if not stack: error("unindent does not match any outer level") # Gather comments if line[0] == "#": comments.append(line) comment_start = comment_start or lineno continue # Get key if line[0] in "'\"": key, after = read_quoted(line) if after[:2] != ": ": error("quoted key must be followed by a ': '") else: value = after[2:].lstrip() else: mo = re.match(r"(.*?):(?:\s+|$)(.*)", line) if mo is None: if ":" in line: raise error("colon at the end of the key should be " "followed by a space or a new line") else: raise error("key followed by colon expected") key, value = mo.groups() # Get value # `value` is lstripped, but may contain whitespace at the end, which is # included in quoted values # Leaves if value.strip(): if value[0] in "'\"": value, after = read_quoted(value) if after.strip(): error("quoted value must be followed by end of line") else: value = {"true": True, "false": False, "null": None }.get(value.strip(), value) stack[-1][1][key] = MsgNode(value, comments or None) # Internal nodes else: space = {} stack[-1][1][key] = MsgNode(space, comments or None) stack.append((indent, space, True)) comments = [] comment_start = None if stack[-1][-1]: raise error("unexpected end of file") check_no_comments() return items def dump(d, indent=""): def quotescape(s, allow_colon): if not s \ or "\n" in s \ or ": " in s and not allow_colon \ or s[0] in " #\"'|" \ or s[-1] in " \t\n": q = '"' if "'" in s else "'" return f"{q}{s.replace(q, 2 * q)}{q}" return s def dumpval(s): trans = {True: "true", False: "false", None: "null", "": '""'} if s in trans: return trans[s] return quotescape(s, True) res = "" for key, node in d.items(): if node.comments is not None: res += "".join(f"{indent}{comment}\n" for comment in node.comments) if isinstance(node.value, dict): res += f"{indent}{key}:\n{dump(node.value, indent + ' ')}" else: res += f"{indent}{quotescape(key, False)}: {dumpval(node.value)}\n" return res trubar-0.3.4/trubar/messages.py000066400000000000000000000055461470120047100165030ustar00rootroot00000000000000import os import sys import re from typing import NamedTuple, Union, Optional, Dict, List import yaml from trubar import jaml from trubar.config import config class MsgNode(NamedTuple): value: Union["MsgDict", str, bool, None] comments: Optional[List[str]] = None MsgDict = Dict[str, MsgNode] PureDict = Dict[str, Union[bool, None, str, "PureDict"]] def load(filename: str) -> MsgDict: if not os.path.exists(filename): print(f"File not found: {filename}") sys.exit(2) is_yaml = os.path.splitext(filename)[1] == ".yaml" try: if is_yaml: with open(filename, encoding=config.encoding) as f: messages = yaml.load(f, Loader=yaml.Loader) else: messages = jaml.readfile(filename, encoding=config.encoding) except (jaml.JamlError, yaml.YAMLError) as exc: print(f"Error in {filename}:\n{exc}") sys.exit(3) if is_yaml: messages = dict_to_msg_nodes(messages) if not check_sanity(messages, filename): sys.exit(4) return messages def check_sanity(message_dict: MsgDict, filename: Optional[str] = None): key_re = re.compile(r"^((def)|(class)) `\w+`") sane = True def fail(msg): nonlocal sane if sane and filename is not None: print(f"Errors in {filename}:") print(msg) sane = False def check_sane(messages: MsgDict, path: str): for key, obj in messages.items(): npath = f"{path}/{key}" if key_re.fullmatch(key) is None: if isinstance(obj.value, dict): fail(f"{npath}: Unexpectedly a namespace") else: if not isinstance(obj.value, dict): fail(f"{npath}: Unexpectedly not a namespace") else: check_sane(obj.value, f"{npath}") for fname, fspace in message_dict.items(): if isinstance(fspace.value, dict): check_sane(fspace.value, fname) return sane def dict_to_msg_nodes(messages: PureDict) -> Dict[str, MsgDict]: return { key: MsgNode(dict_to_msg_nodes(value) if isinstance(value, dict) else value) for key, value in messages.items() } def dict_from_msg_nodes(messages: MsgDict) -> PureDict: return {key: dict_from_msg_nodes(node.value) if isinstance(node.value, dict) else node.value for key, node in messages.items()} def dump(messages: MsgDict, filename: str) -> None: if os.path.splitext(filename)[1] == ".jaml": with open(filename, "w", encoding=config.encoding) as f: f.write(jaml.dump(messages)) else: messages = dict_from_msg_nodes(messages) with open(filename, "wb") as f: f.write(yaml.dump(messages, indent=4, sort_keys=False, encoding="utf-8", allow_unicode=True)) trubar-0.3.4/trubar/tests/000077500000000000000000000000001470120047100154525ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/__init__.py000066400000000000000000000016541470120047100175710ustar00rootroot00000000000000import os import shutil import tempfile import unittest from trubar.messages import dict_to_msg_nodes, dict_from_msg_nodes def yamlized(func): def yamlized_func(*args, **kwargs): args = [dict_to_msg_nodes(arg) if isinstance(arg, dict) else arg for arg in args] res = func(*args, **kwargs) return dict_from_msg_nodes(res) if isinstance(res, dict) else res return yamlized_func class TestBase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.tmpdir = None def tearDown(self) -> None: super().tearDown() if self.tmpdir is not None: shutil.rmtree(self.tmpdir) def prepare_file(self, filename, s): if self.tmpdir is None: self.tmpdir = tempfile.mkdtemp() fn = os.path.join(self.tmpdir, filename) with open(fn, "w", encoding="utf-8") as f: f.write(s) return fn trubar-0.3.4/trubar/tests/shell_tests/000077500000000000000000000000001470120047100200035ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/_config/000077500000000000000000000000001470120047100214075ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/_config/.trubarconfig.yaml000066400000000000000000000000571470120047100250400ustar00rootroot00000000000000auto-import: "from foo import something_fancy" trubar-0.3.4/trubar/tests/shell_tests/_config/config-auto-import.yaml000066400000000000000000000001361470120047100260160ustar00rootroot00000000000000auto-import: "from foo.bar.localization import plurals # pylint: disable=wrong-import-order" trubar-0.3.4/trubar/tests/shell_tests/_config/config-no-prefix.yaml000066400000000000000000000000231470120047100254400ustar00rootroot00000000000000auto-prefix: false trubar-0.3.4/trubar/tests/shell_tests/_config/newly_braced.yaml000066400000000000000000000001221470120047100247240ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: A {'clas' + 's'} attribute trubar-0.3.4/trubar/tests/shell_tests/_config/test.sh000077500000000000000000000044371470120047100227350ustar00rootroot00000000000000echo "(Configuration)" echo "... read configuration" echo "... default is to turn strings to f-strings when needed" print_run 'trubar translate -s ../test_project -d tmp/test_translated newly_braced.yaml -q' set +e grep -q "a = f\"A {'clas' + 's'} attribute\"" tmp/test_translated/__init__.py check_exit_code "Invalid initial configuration? String was not changed to f-string" -ne set -e rm -r tmp/test_translated echo "... but this can be disabled in settings" print_run 'trubar --conf config-no-prefix.yaml translate -s ../test_project -d tmp/test_translated newly_braced.yaml -q' set +e grep -q "a = \"A {'clas' + 's'} attribute\"" tmp/test_translated/__init__.py check_exit_code "Configuration not read? String was still changed to f-string" -ne set -e rm -r tmp/test_translated function check_apples() { if [[ $(head -1 tmp/test_translated/submodule/apples.py) != $1 ]] then echo "Config not imported" echo "" cat tmp/test_translated/submodule/apples.py echo "" exit 1 fi } echo "... test auto import" print_run 'trubar --conf config-auto-import.yaml translate -s ../test_project -d tmp/test_translated translations.yaml -q' check_apples "from foo.bar.localization import plurals # pylint: disable=wrong-import-order" rm -r tmp/test_translated echo "... default configuration in current directory" print_run 'trubar translate -s ../test_project -d tmp/test_translated translations.yaml -q' check_apples "from foo import something_fancy" rm -r tmp/test_translated # Copy project to another location so we can play with adding config file mkdir tmp/tmp2 cp -R ../test_project tmp/tmp2/test_project echo "... default configuration in messages directory" cd tmp print_run 'trubar translate -s tmp2/test_project -d test_translated ../translations.yaml -q' cd .. check_apples "from foo import something_fancy" echo "... default configuration in source directory" cp config-auto-import.yaml tmp/tmp2/test_project/.trubarconfig.yaml mv .trubarconfig.yaml .trubarconfig.bak cd tmp print_run 'trubar translate -s tmp2/test_project -d test_translated ../translations.yaml -q' cd .. mv .trubarconfig.bak .trubarconfig.yaml check_apples "from foo.bar.localization import plurals # pylint: disable=wrong-import-order" rm -r tmp/test_translated tmp/tmp2 trubar-0.3.4/trubar/tests/shell_tests/_config/translations.yaml000066400000000000000000000007531470120047100250210ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: false def `f`: default: false some/directory: true File {x}: Datoteka {x} Not file {x + ".bak"}: Ne datoteka {x + ".bak"} '{"nonsense"}': '{"nesmisel"}' __main__: false Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranče trash/nothing.py: def `f`: To see here: null ', really.': null trubar-0.3.4/trubar/tests/shell_tests/_utils/000077500000000000000000000000001470120047100213025ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/_utils/bad_structure.yaml000066400000000000000000000003541470120047100250360ustar00rootroot00000000000000module1: class `A`: a: k def `foo`: baz b: l def `f`: class `AClass`: message? err: Doesn't look good realy_bad: why namespace?: None module2: def f: True x: None trubar-0.3.4/trubar/tests/shell_tests/_utils/exp/000077500000000000000000000000001470120047100220765ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/_utils/exp/errors_structure.txt000066400000000000000000000003131470120047100262700ustar00rootroot00000000000000Errors in bad_structure.yaml: module1/class `A`/def `foo`: Unexpectedly not a namespace module1/class `A`/def `f`/class `AClass`: Unexpectedly not a namespace module1/realy_bad: Unexpectedly a namespace trubar-0.3.4/trubar/tests/shell_tests/_utils/test.sh000077500000000000000000000003751470120047100226250ustar00rootroot00000000000000echo "(Utils)" echo "... checks for file sanity" set +e print_run 'trubar missing bad_structure.yaml -o tmp/missing.yaml' tmp/errors_structure.txt check_exit_code set -e diff tmp/errors_structure.txt exp/errors_structure.txt rm tmp/errors_structure.txttrubar-0.3.4/trubar/tests/shell_tests/collect/000077500000000000000000000000001470120047100214305ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/collect/exp/000077500000000000000000000000001470120047100222245ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/collect/exp/all_messages.yaml000066400000000000000000000006741470120047100255560ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: null def `f`: default: null some/directory: null File {x}: null Not file {x + ".bak"}: null '{"nonsense"}': null __main__: null Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: null trash/nothing.py: def `f`: To see here: null ', really.': null trubar-0.3.4/trubar/tests/shell_tests/collect/exp/merged_messages.yaml000066400000000000000000000007131470120047100262430ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: null def `f`: default: null some/directory: null File {x}: null Not file {x + ".bak"}: null '{"nonsense"}': null __main__: null Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranče trash/nothing.py: def `f`: To see here: Videti tukaj? ', really.': null trubar-0.3.4/trubar/tests/shell_tests/collect/exp/merged_messages_newer.yaml000066400000000000000000000010011470120047100274320ustar00rootroot00000000000000__init__.py: class `A`: def `f`: default: null some/directory: null File {x}: null Not file {x + ".bak"}: null Extra message: Posebno sporočilo Untranslated messages: null '{"nonsense"}': null __main__: null Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranče trash/nothing.py: def `f`: To see here: Videti tukaj? ', really.': null trubar-0.3.4/trubar/tests/shell_tests/collect/exp/merged_messages_pattern.yaml000066400000000000000000000010011470120047100277670ustar00rootroot00000000000000__init__.py: class `A`: def `f`: default: null some/directory: null File {x}: null Not file {x + ".bak"}: null Extra message: Posebno sporočilo Untranslated messages: null '{"nonsense"}': null __main__: null Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranče trash/nothing.py: def `f`: To see here: Videti tukaj? ', really.': null trubar-0.3.4/trubar/tests/shell_tests/collect/exp/removed.yaml000066400000000000000000000002031470120047100245440ustar00rootroot00000000000000__init__.py: class `A`: def `f`: Extra message: Posebno sporočilo submodule/apples.py: Lemons: Limone trubar-0.3.4/trubar/tests/shell_tests/collect/exp/removed_newer.yaml000066400000000000000000000000501470120047100257440ustar00rootroot00000000000000submodule/apples.py: Lemons: Limone trubar-0.3.4/trubar/tests/shell_tests/collect/exp/removed_output000066400000000000000000000001671470120047100252340ustar00rootroot00000000000000__init__.py/class `A`/def `f`/Extra message not in target structure submodule/apples.py/Lemons not in target structure trubar-0.3.4/trubar/tests/shell_tests/collect/exp/removed_pattern.yaml000066400000000000000000000000501470120047100263010ustar00rootroot00000000000000submodule/apples.py: Lemons: Limone trubar-0.3.4/trubar/tests/shell_tests/collect/exp/submodule_messages.yaml000066400000000000000000000000471470120047100267770ustar00rootroot00000000000000submodule/apples.py: Oranges: null trubar-0.3.4/trubar/tests/shell_tests/collect/some_messages.yaml000066400000000000000000000010241470120047100251430ustar00rootroot00000000000000__init__.py: class `A`: def `f`: default: null some/directory: null File {x}: null Not file {x + ".bak"}: null Extra message: Posebno sporočilo Untranslated messages: null '{"nonsense"}': null __main__: null Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranče Lemons: Limone trash/nothing.py: def `f`: To see here: Videti tukaj? ', really.': null trubar-0.3.4/trubar/tests/shell_tests/collect/test.sh000077500000000000000000000064001470120047100227460ustar00rootroot00000000000000echo "Collect" print_run 'trubar collect -s ../test_project tmp/messages.yaml -q' diff tmp/messages.yaml exp/all_messages.yaml if [ -n "`trubar collect -s ../test_project tmp/messages.yaml -q`" ] then echo "Not quiet." exit 1 fi if [ -z "`trubar collect -s ../test_project tmp/messages.yaml`" ] then echo "Not loud." exit 1 fi rm tmp/messages.yaml echo "... with pattern" print_run 'trubar collect -s ../test_project tmp/messages.yaml -p submodule -q' diff tmp/messages.yaml exp/submodule_messages.yaml rm tmp/messages.yaml echo "... merge with existing file" cp some_messages.yaml tmp/some_messages.yaml print_run 'trubar collect -s ../test_project -r tmp/removed.yaml tmp/some_messages.yaml -q' diff tmp/some_messages.yaml exp/merged_messages.yaml diff tmp/removed.yaml exp/removed.yaml rm tmp/some_messages.yaml tmp/removed.yaml echo "... merge with existing file, with --newer" cp some_messages.yaml tmp/some_messages.yaml touch -t 11111230.00 ../test_project/__init__.py touch -t 11111239.00 tmp/some_messages.yaml touch -t 11111239.00 ../test_project/submodule/apples.py touch -t 11111245.00 ../test_project/trash/nothing.py print_run 'trubar collect -s ../test_project -r tmp/removed.yaml --newer tmp/some_messages.yaml -q' diff tmp/some_messages.yaml exp/merged_messages_newer.yaml diff tmp/removed.yaml exp/removed_newer.yaml rm tmp/some_messages.yaml tmp/removed.yaml echo "... merge with existing file, default removed file" cp some_messages.yaml tmp/some_messages.yaml print_run 'trubar collect -s ../test_project tmp/some_messages.yaml -q' diff tmp/some_messages.yaml exp/merged_messages.yaml diff tmp/removed-from-some_messages.yaml exp/removed.yaml rm tmp/some_messages.yaml tmp/removed-from-some_messages.yaml echo "... merge with existing file, with pattern" cp some_messages.yaml tmp/some_messages.yaml print_run 'trubar collect -s ../test_project -r tmp/removed.yaml -p submodule tmp/some_messages.yaml -q' diff tmp/some_messages.yaml exp/merged_messages_pattern.yaml diff tmp/removed.yaml exp/removed_pattern.yaml rm tmp/some_messages.yaml tmp/removed.yaml echo "... merge with existing file, dry run" cp some_messages.yaml tmp/some_messages.yaml print_run 'trubar collect -s ../test_project -r tmp/removed.yaml -n -q tmp/some_messages.yaml' diff tmp/some_messages.yaml some_messages.yaml diff tmp/removed.yaml exp/removed.yaml rm tmp/some_messages.yaml tmp/removed.yaml echo "... merge with existing file, dry run, default removed file" cp some_messages.yaml tmp/some_messages.yaml print_run 'trubar collect -s ../test_project tmp/some_messages.yaml -n -q' tmp/removed_output diff tmp/some_messages.yaml some_messages.yaml diff tmp/removed-from-some_messages.yaml exp/removed.yaml rm tmp/some_messages.yaml tmp/removed-from-some_messages.yaml echo "... invalid source dir" set +e print_run 'trubar collect -s ../test_project/__init__.py tmp/messages.yaml -q' /dev/null check_exit_code print_run 'trubar collect -s ../test_project_not tmp/messages.yaml -q' /dev/null check_exit_code set -e set +e echo "... invalid source dir correction" cp some_messages.yaml tmp/some_messages.yaml print_run 'trubar collect -s .. tmp/some_messages.yaml' tmp/output.txt check_exit_code grep -q "instead" tmp/output.txt check_exit_code "No error message" -ne set -e rm tmp/some_messages.yaml tmp/output.txt trubar-0.3.4/trubar/tests/shell_tests/merge/000077500000000000000000000000001470120047100211025ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/merge/exp/000077500000000000000000000000001470120047100216765ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/merge/exp/errors.txt000066400000000000000000000002501470120047100237500ustar00rootroot00000000000000__init__.py/class `A`/def `f`/def `default` not in target structure __init__.py/def `extranamespace` not in target structure trash/nothing.py/f not in target structure trubar-0.3.4/trubar/tests/shell_tests/merge/exp/unused.yaml000066400000000000000000000002621470120047100240650ustar00rootroot00000000000000__init__.py: class `A`: def `f`: def `default`: a: b def `extranamespace`: with: something trash/nothing.py: f: a message trubar-0.3.4/trubar/tests/shell_tests/merge/exp/updated_translations.jaml000066400000000000000000000011171470120047100267720ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: false def `f`: # This is now translated # It was not translated before default: privzeto some/directory: false File {x}: Datoteka {x} Not file {x + ".bak"}: Ne pa datoteka {x + ".bak"} {"nonsense"}: {"nesmisel"} __main__: false Please don't run this.: null Import it, if you must.: null submodule/apples.py: # Namreč dve. Oranges: Pomaranči trash/nothing.py: def `f`: To see here: null , really.: null trubar-0.3.4/trubar/tests/shell_tests/merge/exp/updated_translations.yaml000066400000000000000000000007621470120047100270160ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: false def `f`: default: privzeto some/directory: false File {x}: Datoteka {x} Not file {x + ".bak"}: Ne pa datoteka {x + ".bak"} '{"nonsense"}': '{"nesmisel"}' __main__: false Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranči trash/nothing.py: def `f`: To see here: null ', really.': null trubar-0.3.4/trubar/tests/shell_tests/merge/exp/updated_translations_faulty.yaml000066400000000000000000000007441470120047100304020ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: false def `f`: default: false some/directory: true File {x}: Datoteka {x} Not file {x + ".bak"}: Ni datoteka '{"nonsense"}': '{"nesmisel"}' __main__: false Please don't run this.: null Import it, if you must.: translated submodule/apples.py: Oranges: Pomaranče trash/nothing.py: def `f`: To see here: null ', really.': null trubar-0.3.4/trubar/tests/shell_tests/merge/exp/updated_translations_submodule.jaml000066400000000000000000000007701470120047100310550ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: false def `f`: default: false some/directory: true File {x}: Datoteka {x} Not file {x + ".bak"}: Ne datoteka {x + ".bak"} {"nonsense"}: {"nesmisel"} __main__: false Please don't run this.: null Import it, if you must.: null submodule/apples.py: # Namreč dve. Oranges: Pomaranči trash/nothing.py: def `f`: To see here: null , really.: null trubar-0.3.4/trubar/tests/shell_tests/merge/faulty_translations.yaml000066400000000000000000000007451470120047100261010ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: null def `f`: def `default`: a: b some/directory: null File {x}: null Not file {x + ".bak"}: Ni datoteka extra message: null __main__: null def `extranamespace`: with: something Please don't run this.: null Import it, if you must.: translated submodule/apples.py: Oranges: Pomaranče trash/nothing.py: f: a messagetrubar-0.3.4/trubar/tests/shell_tests/merge/new_translations.jaml000066400000000000000000000006001470120047100253350ustar00rootroot00000000000000__init__.py: class `A`: def `f`: # This is now translated # It was not translated before default: "privzeto" some/directory: false File {x}: Datoteka {x} Not file {x + ".bak"}: Ne pa datoteka {x + ".bak"} {"nonsense"}: null submodule/apples.py: # Namreč dve. Oranges: Pomaranči trubar-0.3.4/trubar/tests/shell_tests/merge/test.sh000077500000000000000000000043351470120047100224250ustar00rootroot00000000000000echo "Merge" cp translations.yaml tmp/translations-copy.yaml print_run 'trubar merge new_translations.jaml translations.yaml -o tmp/updated_translations.yaml' diff translations.yaml tmp/translations-copy.yaml diff tmp/updated_translations.yaml exp/updated_translations.yaml rm tmp/updated_translations.yaml print_run 'trubar merge new_translations.jaml translations.yaml -o tmp/updated_translations.jaml' diff translations.yaml tmp/translations-copy.yaml diff tmp/updated_translations.jaml exp/updated_translations.jaml rm tmp/updated_translations.jaml echo "... in place" cp translations.yaml tmp/translations.yaml print_run 'trubar merge new_translations.jaml tmp/translations.yaml' diff tmp/translations.yaml exp/updated_translations.yaml rm tmp/translations.yaml echo "... with pattern" print_run 'trubar merge new_translations.jaml translations.yaml -o updated_translations.jaml -p submodule' diff updated_translations.jaml exp/updated_translations_submodule.jaml rm updated_translations.jaml echo "... with errors" cp translations.yaml tmp/translations-copy.yaml print_run 'trubar merge faulty_translations.yaml tmp/translations-copy.yaml -o tmp/updated_translations.yaml -u tmp/unused.yaml' tmp/errors.txt diff tmp/translations-copy.yaml translations.yaml diff tmp/updated_translations.yaml exp/updated_translations_faulty.yaml diff tmp/unused.yaml exp/unused.yaml if [[ ! -z $(cat tmp/errors.txt) ]] then echo "merge mustn't output unused items when writing them to a file" cat tmp/errors.txt exit 1 fi rm tmp/updated_translations.yaml tmp/unused.yaml tmp/errors.txt echo "... dry-run, with errors" print_run 'trubar merge faulty_translations.yaml tmp/translations-copy.yaml -u tmp/unused.yaml -n' tmp/errors.txt diff tmp/translations-copy.yaml translations.yaml diff tmp/unused.yaml exp/unused.yaml if [[ ! -z $(cat tmp/errors.txt) ]] then echo "merge mustn't output unused items when writing them to a file" cat tmp/errors.txt exit 1 fi rm tmp/unused.yaml tmp/errors.txt echo "... dry-run, with errors" print_run 'trubar merge faulty_translations.yaml tmp/translations-copy.yaml -n' tmp/errors.txt diff tmp/translations-copy.yaml translations.yaml diff tmp/errors.txt tmp/errors.txt rm tmp/translations-copy.yaml tmp/errors.txt trubar-0.3.4/trubar/tests/shell_tests/merge/translations.yaml000066400000000000000000000007531470120047100245140ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: false def `f`: default: false some/directory: true File {x}: Datoteka {x} Not file {x + ".bak"}: Ne datoteka {x + ".bak"} '{"nonsense"}': '{"nesmisel"}' __main__: false Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranče trash/nothing.py: def `f`: To see here: null ', really.': null trubar-0.3.4/trubar/tests/shell_tests/missing/000077500000000000000000000000001470120047100214545ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/missing/all_messages.yaml000066400000000000000000000006741470120047100250060ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: null def `f`: default: null some/directory: null File {x}: null Not file {x + ".bak"}: null '{"nonsense"}': null __main__: null Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: null trash/nothing.py: def `f`: To see here: null ', really.': null trubar-0.3.4/trubar/tests/shell_tests/missing/exp/000077500000000000000000000000001470120047100222505ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/missing/exp/missing.jaml000066400000000000000000000003261470120047100245670ustar00rootroot00000000000000__init__.py: # Seriously? Please don't run this.: null Import it, if you must.: null # Not? Really? trash/nothing.py: def `f`: # Or to hear To see here: null , really.: null trubar-0.3.4/trubar/tests/shell_tests/missing/exp/missing.yaml000066400000000000000000000002411470120047100246020ustar00rootroot00000000000000__init__.py: Please don't run this.: null Import it, if you must.: null trash/nothing.py: def `f`: To see here: null , really.: null trubar-0.3.4/trubar/tests/shell_tests/missing/exp/missing_trash.jaml000066400000000000000000000001651470120047100257710ustar00rootroot00000000000000# Not? Really? trash/nothing.py: def `f`: # Or to hear To see here: null , really.: null trubar-0.3.4/trubar/tests/shell_tests/missing/test.sh000077500000000000000000000016221470120047100227730ustar00rootroot00000000000000# to be run from trubar/tests/shell_test_files echo "Missing" echo "... all missing" print_run 'trubar missing all_messages.yaml -o tmp/missing.yaml' diff tmp/missing.yaml all_messages.yaml rm tmp/missing.yaml echo "... some missing" print_run 'trubar missing translations.jaml -o tmp/missing.jaml' diff tmp/missing.jaml exp/missing.jaml rm tmp/missing.jaml echo "... some missing, pattern" print_run 'trubar missing translations.jaml -o tmp/missing.jaml -p trash' diff tmp/missing.jaml exp/missing_trash.jaml rm tmp/missing.jaml echo "... given all messages" print_run 'trubar missing translations.jaml -m all_messages.yaml -o tmp/missing.jaml' diff tmp/missing.jaml exp/missing.jaml rm tmp/missing.jaml echo "... given all messages and pattern" print_run 'trubar missing translations.jaml -m all_messages.yaml -o tmp/missing.jaml -p trash' diff tmp/missing.jaml exp/missing_trash.jaml rm tmp/missing.jaml trubar-0.3.4/trubar/tests/shell_tests/missing/translations.jaml000066400000000000000000000010401470120047100250350ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: false def `f`: default: false some/directory: true File {x}: Datoteka {x} Not file {x + ".bak"}: Ne datoteka {x + ".bak"} '{"nonsense"}': '{"nesmisel"}' __main__: false # Seriously? Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranče # Not? Really? trash/nothing.py: def `f`: # Or to hear To see here: null ', really.': null trubar-0.3.4/trubar/tests/shell_tests/stat/000077500000000000000000000000001470120047100207565ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/stat/exp/000077500000000000000000000000001470120047100215525ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/stat/exp/all.txt000066400000000000000000000002651470120047100230660ustar00rootroot00000000000000Total messages: 12 Translated: 4 33.3% Kept unchanged: 1 8.3% Programmatic: 3 25.0% Total completed: 8 66.7% Untranslated: 4 33.3% trubar-0.3.4/trubar/tests/shell_tests/stat/exp/nomodule.txt000066400000000000000000000000141470120047100241300ustar00rootroot00000000000000No messages trubar-0.3.4/trubar/tests/shell_tests/stat/exp/submodule.txt000066400000000000000000000002641470120047100243140ustar00rootroot00000000000000Total messages: 1 Translated: 1 100.0% Kept unchanged: 0 0.0% Programmatic: 0 0.0% Total completed: 1 100.0% Untranslated: 0 0.0% trubar-0.3.4/trubar/tests/shell_tests/stat/test.sh000077500000000000000000000005611470120047100222760ustar00rootroot00000000000000# to be run from trubar/tests/shell_test_files echo "Stat" print_run 'trubar stat translations.yaml' tmp/all.txt diff tmp/all.txt exp/all.txt print_run 'trubar stat translations.yaml -p submodule' tmp/submodule.txt diff tmp/submodule.txt exp/submodule.txt print_run 'trubar stat translations.yaml -p nomodule' tmp/nomodule.txt diff tmp/nomodule.txt exp/nomodule.txttrubar-0.3.4/trubar/tests/shell_tests/stat/translations.yaml000066400000000000000000000007531470120047100243700ustar00rootroot00000000000000__init__.py: class `A`: A class attribute: false def `f`: default: false some/directory: true File {x}: Datoteka {x} Not file {x + ".bak"}: Ne datoteka {x + ".bak"} '{"nonsense"}': '{"nesmisel"}' __main__: false Please don't run this.: null Import it, if you must.: null submodule/apples.py: Oranges: Pomaranče trash/nothing.py: def `f`: To see here: null ', really.': null trubar-0.3.4/trubar/tests/shell_tests/template/000077500000000000000000000000001470120047100216165ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/template/exp/000077500000000000000000000000001470120047100224125ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/template/exp/template.jaml000066400000000000000000000002671470120047100250770ustar00rootroot00000000000000a: b: null e: null f: null def `g`: h: null # This class may be left untranslated, # depending on whatever class `q`: # In particular this one s: null trubar-0.3.4/trubar/tests/shell_tests/template/test.sh000077500000000000000000000002221470120047100231300ustar00rootroot00000000000000echo "Template" print_run 'trubar template translations.jaml -o tmp/template.jaml' diff tmp/template.jaml exp/template.jaml rm tmp/template.jaml trubar-0.3.4/trubar/tests/shell_tests/template/translations.jaml000066400000000000000000000005001470120047100251770ustar00rootroot00000000000000a: b: c d: false e: true f: null def `g`: h: i j: false class `k`: l: false def `m`: n: false o: false p: false # This class may be left untranslated, # depending on whatever class `q`: r: false # In particular this one s: true trubar-0.3.4/trubar/tests/shell_tests/test_project/000077500000000000000000000000001470120047100225105ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/test_project/__init__.py000066400000000000000000000007021470120047100246200ustar00rootroot00000000000000"""Doc string""" import os class A: '''Doc string''' a = "A class attribute" def f(self, x="default"): "Doc string" t = os.listdir("some/directory") for x in t: print(f"File {x}") print(f'Not file {x + ".bak"}') if x.endswith(f"""{"nonsense"}"""): return x if __name__ == "__main__": print("Please don't run this.") print('Import it, if you must.')trubar-0.3.4/trubar/tests/shell_tests/test_project/submodule/000077500000000000000000000000001470120047100245075ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/test_project/submodule/apples.py000066400000000000000000000000201470120047100263350ustar00rootroot00000000000000print("Oranges")trubar-0.3.4/trubar/tests/shell_tests/test_project/submodule/nomessages.py000066400000000000000000000000121470120047100272160ustar00rootroot00000000000000print(42) trubar-0.3.4/trubar/tests/shell_tests/test_project/submodule/some_file.txt000066400000000000000000000000031470120047100272030ustar00rootroot00000000000000123trubar-0.3.4/trubar/tests/shell_tests/test_project/submodule/subdirectory/000077500000000000000000000000001470120047100272255ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/shell_tests/test_project/submodule/subdirectory/Primoz-Trubar.jpg000066400000000000000000000171311470120047100324470ustar00rootroot00000000000000JFIFHHExifMM*JR(iZHHc8Photoshop 3.08BIM8BIM%ُ B~c }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzC   %# , #&')*)-0-(0%()(C   ((((((((((((((((((((((((((((((((((((((((((((((((((( ??lc GIBකHvUr߯֩+eMn3+ -7IKqr?Jy?KY<2c,N޽ %$b&G"[%GOL9IDW*wr1ߓӎn)npK-̷ K S^= i8$ EGQLA,"2g#$N9=d2kVwrO<++Rpn#4r&ÙjiZ4wW%ߘ"gěO `ؤ㮃Lf OQ"`cx*p˓C҇MpRM%4j,Qnsd#Y *UY'v8!RO5wc#$g`3-tv]#|w R3kwQδ'I(hH.l%Cpw-$S81\EEV3-.#_;6YcV1eޭߡ(cJVK{;˄`т> pV9ϖ=w;3,CYi˨U G1mlEUԛY_$r+1g`. AQ9hnltY4XTpDr_^'}+ljKuəfy^HT 5*Zu+hի9Y۔1ķRCz]X(gfTY@=iw[^EUe_c'MYUAwW>vdXxV^c%epzUYMYx"x/$1\edJ*f hAl:?Qzwc[IݭI.P1CSݻ23nȝ].O%4L>ؓ讣$cҡR61ڦXVtDG-]m:hlU<9+\y<ۛY[r2T3gEX%%a\XKI]tvriXw'+^Qsbp-lUT?6g9yZ0T=.E;"G!Yr00`Nsv7W7<I }ݸ0r=pNr*ԒD4Nw)cXǶT*[G ?ܶ8o_1#b:tkxn{[x%.fi#RE% հ=MWRM,b֍% `mҫd/91`NϭMʱ[Z3̷Ip (C}[T\wvqber[ZvM_Qll-l$I( 8,(rlIXK>$G%KƎI8'Ic̓@FiXbNvozαnOdSt1J1dv}lMVuk l0sG^1EBɆ.dg< =g5|NxJa#b ]i'qNzw=2?EbY-"ԥWE{Q5b䢌~\\# :dܭdTh'(mHD ) 'dJa#mDy!g*̨3GsOE?ި)w6͞&s~K&)mɆM2 `tKRbdz\\*+,' {ry¹BI|Hc+2X~vH]Lb%U[t%J*@L}wc𪋺ܵual~bL {OT8t:ܦe˸r žrWݗO|79YOV;HX zʲ[, $\ⵊ*O,[.,}H׎ s-Gȳ're(IceĜX#g kxgqe,JQ ڈcm 3-?q(EͲ*ӱ}ix_l/L:+`8'c>13%|*%3>$r3t+6&w?n$>f7T= .M4 C/U -xյH6Iovk"ɷ1zLޚ;ߵdhre}e$n&,xTrpبZCmqrAYTEOYO;zC{o1+"/.-AYA=ǘdj0sО++^ЕNjc'8cC^eEyҤfG'ig{atǵTؤ:E{8qy,I gqz&tzguhш܀%_,rBy=zQ{ukϱ1+ UXq3'w!RP/p "m'zQ}.gGfxR]+W8#~0aӌ?")V)x`_Kuyq- WSm6Qǡ=I4aGSk%u]IZBiH~`Q[*[>l/h.`sAV%=*ՋZ~([]ZIRě=ƹK%7f(`A 3HCkq9HV%ORԣwS6w]隐,JdV#AA>'Dmyt}yGm֒Z$>jKikcfcw֌Ik6yglIB<?Mר qs2IYA}jR(Ǒu}57 \N!Q՗8Z%IZMJBZg2YA80ǧRyޥe-m{2'3NHFX0|\~_1y4E˒HmJ ÒG8W8p )1mʶ5<"g&ۼ |Cd. 78Bk⾹ZEnT/1_ [KrS5mg4# <"vg'H<QzS+trmF;l/mlg*Y#省nv!DCfj]`F21+9T*<@ӚzNNkOyc=̲iJ KeQr^.7sY4JCJ-suaՍB;OJ́tLFa2Kn *; s'8[-F6k!ERR#k& sPw)"Kmg UR8щw%='R<+#C]ib6&6q?pkerZ)fɄe z{jC؛M6) ^BE0hnv)޸FI}+mgc+"pHW>^TjnrxK 1i]?2%Nr:6-2gA4{"dep܃@Ϧ}kf&,h=A,rwc  px j_K;v+!K{ȼ *}@#:Bl1"W68'0n$Y .3cn=p)zZ*mp(yG{lߞʐcs\(vxXVVm!dLjZnm'p7[erNA0;cM.lmVxh{_;r݆P1G+`Efyڃ#͚"%kp pFN88۰6lʪ>%@y@#''`qPZh&c Đ9Wz- Z,-֚:# QZ0pS)`2;uGTu 5{YKdL|#vT䴲zݝ]^Dn]&K>ܧ۫CG⡔ L?po&}shnegen9}НіŋKcd݋:9S-tWQ \͆ey8?unqgڤYadm0s9w]$nt`ԩz>4W&[iMs-L'dFqd7#YjvBn.x pV9ϖ=w;3,CYi˨U G1mlEUԛY_$r+1g`. AQ9hnltY4XTpDr_^'}+ljKuəfy^HT 5*Zu+hի9Y۔1ķRCz]X(gfTY@=iw[^EUe_c'MYUAwW>vdXxV^c%epzUYMYx"x/$1\edJ*f hAl:?Qzwc[IݭI.P1CSݻ23nȝ].O%4L>ؓ讣$cҡR61ڦXVtDG-]m:hlU<9+\y<ۛY[r2T3gEX%%a\XKI]tvriXw'+^Qsbp-lUT?6g9yZ0T=.E;"G!Yr00`Nsv7W7<I }ݸ0r=pNr*ԒD4Nw)cXǶT*[G ?ܶ8o_1#b:tkxn{[x%.fi#RE% հ=MWRM,b֍% `mҫd/91`NϭMʱ[Z3̷Ip (C}[T\wvqber[ZvM_Qll-l$I( 8,(rlIXK>$G%KƎI8'Ic̓@FiXbNvozαnOdSt1J1dv}lMVuk l0sG^1EBɆ.dg< =g5|NxJa#b ]i'qNzw=2?EbY-"ԥWE{Q5b䢌~\\# :dܭdTh'(mHD ) 'dJa#mDy!g*̨3GsOE?ި)w6͞&s~K&)mɆM2 `tKRbdz\\*+,' {ry¹BI|Hc+2X~vH]Lb%U[t%J*@L}wc𪋺ܵual~bL {OT8t:ܦe˸r žrWݗO|79YOV;HX zʲ[, $\ⵊ*O,[.,}H׎ s-Gȳ're(IceĜX#g kxgqe,JQ ڈcm 3-?q(EͲ*ӱ}ix_l/L:+`8'c>13%|*%3>$r3t+6&w?n$>f7T= .M4 C/U -xյH6Iovk"ɷ1zLޚ;ߵdhre}e$n&,xTrpبZCmqrAYTEOYO;zC{o1+"/.-AYA=ǘdj0sО++^ЕNjc'8cC^eEyҤfG'ig{atǵTؤ:E{8qy,I gqz&tzguhш܀%_,rBy=zQ{ukϱ1+ UXq3'w!RP/p "m'zQ}.gGfxR]+W8#~0aӌ?")V)x`_Kuyq- WSm6Qǡ=I4aGSk%u]IZBiH~`Q[*[>l/h.`sAV%=*ՋZ~([]ZIRě=ƹK%7f(`A 3HCkq9HV%ORԣwS6w]隐,JdV#AA>'Dmyt}yGm֒Z$>jKikcfcw֌Ik6yglIB<?Mר qs2IYA}jR(Ǒu}57 \N!Q՗8Z%IZMJBZg2YA80ǧRyޥe-m{2'3NHFX0|\~_1y4E˒HmJ ÒG8W8p )1mʶ5<"g&ۼ |Cd. 78Bk⾹ZEnT/1_ [KrS5mg4# <"vg'H<QzS+trmF;l/mlg*Y#省nv!DCfj]`F21+9T*<@ӚzNNkOyc=̲iJ KeQr^.7sY4JCJ-suaՍB;OJ́tLFa2Kn *; s'8[-F6k!ERR#k& sPw)"Kmg UR8щw%='R<+#C]ib6&6q?pkerZ)fɄe z{jC؛M6) ^BE0hnv)޸FI}+mgc+"pHW>^TjnrxK 1i]?2%Nr:6-2gA4{"dep܃@Ϧ}kf&,h=A,rwc  px j_K;v+!K{ȼ *}@#:Bl1"W68'0n$Y .3cn=p)zZ*mp(yG{lߞʐcs\(vxXVVm!dLjZnm'p7[erNA0;cM.lmVxh{_;r݆P1G+`Efyڃ#͚"%kp pFN88۰6lʪ>%@y@#''`qPZh&c Đ9Wz- Z,-֚:# QZ0pS)`2;uGTu 5{YKdL|#vT䴲zݝ]^Dn]&K>ܧ۫CG⡔ L?po&}shnegen9}НіŋKcd݋:9S-tWQ \͆ey8?unqgڤYadm0s9w]$nt`ԩz>4W&[iMs-L'dFqd7#YjvBn.x pV9ϖ=w;3,CYi˨U G1mlEUԛY_$r+1g`. AQ9hnltY4XTpDr_^'}+ljKuəfy^HT 5*Zu+hի9Y۔1ķRCz]X(gfTY@=iw[^EUe_c'MYUAwW>vdXxV^c%epzUYMYx"x/$1\edJ*f hAl:?Qzwc[IݭI.P1CSݻ23nȝ].O%4L>ؓ讣$cҡR61ڦXVtDG-]m:hlU<9+\y<ۛY[r2T3gEX%%a\XKI]tvriXw'+^Qsbp-lUT?6g9yZ0T=.E;"G!Yr00`Nsv7W7<I }ݸ0r=pNr*ԒD4Nw)cXǶT*[G ?ܶ8o_1#b:tkxn{[x%.fi#RE% հ=MWRM,b֍% `mҫd/91`NϭMʱ[Z3̷Ip (C}[T\wvqber[ZvM_Qll-l$I( 8,(rlIXK>$G%KƎI8'Ic̓@FiXbNvozαnOdSt1J1dv}lMVuk l0sG^1EBɆ.dg< =g5|NxJa#b ]i'qNzw=2?EbY-"ԥWE{Q5b䢌~\\# :dܭdTh'(mHD ) 'dJa#mDy!g*̨3GsOE?ި)w6͞&s~K&)mɆM2 `tKRbdz\\*+,' {ry¹BI|Hc+2X~vH]Lb%U[t%J*@L}wc𪋺ܵual~bL {OT8t:ܦe˸r žrWݗO|79YOV;HX zʲ[, $\ⵊ*O,[.,}H׎ s-Gȳ're(IceĜX#g kxgqe,JQ ڈcm 3-?q(EͲ*ӱ}ix_l/L:+`8'c>13%|*%3>$r3t+6&w?n$>f7T= .M4 C/U -xյH6Iovk"ɷ1zLޚ;ߵdhre}e$n&,xTrpبZCmqrAYTEOYO;zC{o1+"/.-AYA=ǘdj0sО++^ЕNjc'8cC^eEyҤfG'ig{atǵTؤ:E{8qy,I gqz&tzguhш܀%_,rBy=zQ{ukϱ1+ UXq3'w!RP/p "m'zQ}.gGfxR]+W8#~0aӌ?")V)x`_Kuyq- WSm6Qǡ=I4aGSk%u]IZBiH~`Q[*[>l/h.`sAV%=*ՋZ~([]ZIRě=ƹK%7f(`A 3HCkq9HV%ORԣwS6w]隐,JdV#AA>'Dmyt}yGm֒Z$>jKikcfcw֌Ik6yglIB<?Mר qs2IYA}jR(Ǒu}57 \N!Q՗8Z%IZMJBZg2YA80ǧRyޥe-m{2'3NHFX0|\~_1y4E˒HmJ ÒG8W8p )1mʶ5<"g&ۼ |Cd. 78Bk⾹ZEnT/1_ [KrS5mg4# <"vg'H<QzS+trmF;l/mlg*Y#省nv!DCfj]`F21+9T*<@ӚzNNkOyc=̲iJ KeQr^.7sY4JCJ-suaՍB;OJ́tLFa2Kn *; s'8[-F6k!ERR#k& sPw)"Kmg UR8щw%='R<+#C]ib6&6q?pkerZ)fɄe z{jC؛M6) ^BE0hnv)޸FI}+mgc+"pHW>^TjnrxK 1i]?2%Nr:6-2gA4{"dep܃@Ϧ}kf&,h=A,rwc  px j_K;v+!K{ȼ *}@#:Bl1"W68'0n$Y .3cn=p)zZ*mp(yG{lߞʐcs\(vxXVVm!dLjZnm'p7[erNA0;cM.lmVxh{_;r݆P1G+`Efyڃ#͚"%kp pFN88۰6lʪ>%@y@#''`qPZh&c Đ9Wz- Z,-֚:# QZ0pS)`2;uGTu 5{YKdL|#vT䴲zݝ]^Dn]&K>ܧ۫CG⡔ L?po&}shnegen9}НіŋKcd݋:9S-tWQ \͆ey8?unqgڤYadm0s9w]$nt`ԩz>4W&[iMs-L'dFqd7#YjvBn.x pV9ϖ=w;3,CYi˨U G1mlEUԛY_$r+1g`. AQ9hnltY4XTpDr_^'}+ljKuəfy^HT 5*Zu+hի9Y۔1ķRCz]X(gfTY@=iw[^EUe_c'MYUAwW>vdXxV^c%epzUYMYx"x/$1\edJ*f hAl:?Qzwc[IݭI.P1CSݻ23nȝ].O%4L>ؓ讣$cҡR61ڦXVtDG-]m:hlU<9+\y<ۛY[r2T3gEX%%a\XKI]tvriXw'+^Qsbp-lUT?6g9yZ0T=.E;"G!Yr00`Nsv7W7<I }ݸ0r=pNr*ԒD4Nw)cXǶT*[G ?ܶ8o_1#b:tkxn{[x%.fi#RE% հ=MWRM,b֍% `mҫd/91`NϭMʱ[Z3̷Ip (C}[T\wvqber[ZvM_Qll-l$I( 8,(rlIXK>$G%KƎI8'Ic̓@FiXbNvozαnOdSt1J1dv}lMVuk l0sG^1EBɆ.dg< =g5|NxJa#b ]i'qNzw=2?EbY-"ԥWE{Q5b䢌~\\# :dܭdTh'(mHD ) 'dJa#mDy!g*̨3GsOE?ި)w6͞&s~K&)mɆM2 `tKRbdz\\*+,' {ry¹BI|Hc+2X~vH]Lb%U[t%J*@L}wc𪋺ܵual~bL {OT8t:ܦe˸r žrWݗO|79YOV;HX zʲ[, $\ⵊ*O,[.,}H׎ s-Gȳ're(IceĜX#g kxgqe,JQ ڈcm 3-?q(EͲ*ӱ}ix_l/L:+`8'c>13%|*%3>$r3t+6&w?n$>f7T= .M4 C/U -xյH6Iovk"ɷ1zLޚ;ߵdhre}e$n&,xTrpبZCmqrAYTEOYO;zC{o1+"/.-AYA=ǘdj0sО++^ЕNjc'8cC^eEyҤfG'ig{atǵTؤ:E{8qy,I gqz&tzguhш܀%_,rBy=zQ{ukϱ1+ UXq3'w!RP/p "m'zQ}.gGfxR]+W8#~0aӌ?")V)x`_Kuyq- WSm6Qǡ=I4aGSk%u]IZBiH~`Q[*[>l/h.`sAV%=*ՋZ~([]ZIRě=ƹK%7f(`A 3HCkq9HV%ORԣwS6w]隐,JdV#AA>'Dmyt}yGm֒Z$>jKikcfcw֌Ik6yglIB<?Mר qs2IYA}jR(Ǒu}57 \N!Q՗8Z%IZMJBZg2YA80ǧRyޥe-m{2'3NHFX0|\~_1y4E˒HmJ ÒG8W8p )1mʶ5<"g&ۼ |Cd. 78Bk⾹ZEnT/1_ [KrS5mg4# <"vg'H<QzS+trmF;l/mlg*Y#省nv!DCfj]`F21+9T*<@ӚzNNkOyc=̲iJ KeQr^.7sY4JCJ-suaՍB;OJ́tLFa2Kn *; s'8[-F6k!ERR#k& sPw)"Kmg UR8щw%='R<+#C]ib6&6q?pkerZ)fɄe z{jC؛M6) ^BE0hnv)޸FI}+mgc+"pHW>^TjnrxK 1i]?2%Nr:6-2gA4{"dep܃@Ϧ}kf&,h=A,rwc  px j_K;v+!K{ȼ *}@#:Bl1"W68'0n$Y .3cn=p)zZ*mp(yG{lߞʐcs\(vxXVVm!dLjZnm'p7[erNA0;cM.lmVxh{_;r݆P1G+`Efyڃ#͚"%kp pFN88۰6lʪ>%@y@#''`qPZh&c Đ9Wz- Z,-֚:# QZ0pS)`2;uGTu 5{YKdL|#vT䴲zݝ]^Dn]&K>ܧ۫CG⡔ L?po&}shnegen9}НіŋKcd݋:9S-tWQ \͆ey8?unqgڤYadm0s9w]$nt`ԩz>4W&[iMs-L'dFqd7#YjvBn.x " $1 if [ -z $2 ] then ${1} else ${1} > $2 2>&1 fi } sysdiff=`which diff` function diff() { oldstate="$(set +o)" set +e eval $sysdiff "$@" > /dev/null if [ $? -ne 0 ] then echo diff $@ eval $sysdiff "$@" exit 1 fi eval "$oldstate" } function check_exit_code() { # Test that exit code is nonzero (if $2 is omitted) or zero (if $2 is -ne) # $1 is error message; see default below :) if [ $? ${2:--eq} 0 ] then echo ${1:-"Non-zero exit code expected"} exit 1 fi } cd shell_tests for d in ${1:-*}/ do if [ ! -f $d/test.sh ]; then continue; fi cd $d rm -rf tmp mkdir tmp ( set -e . test.sh ) if [ $? -ne 0 ] then echo "" echo "*** FAIL!" test ! -z "$FAILED" && FAILED="$FAILED, " FAILED=$FAILED${d%/} else rm -rf tmp fi cd .. echo "" echo "" done cd .. if [ ! -z "$FAILED" ] then echo "Failed tests: $FAILED" exit 1 else echo "Success." fi trubar-0.3.4/trubar/tests/test_config.py000066400000000000000000000205171470120047100203350ustar00rootroot00000000000000import os import dataclasses import unittest from unittest.mock import patch from trubar.config import Configuration, LanguageDef from trubar.tests import TestBase class ConfigTest(TestBase): def prepare(self, s): # pylint: disable=attribute-defined-outside-init self.fn = self.prepare_file("test.yaml", s) def test_proper_file(self): config = Configuration() self.prepare("smart_quotes: false\n\nencoding: cp-1234") config.update_from_file(self.fn) self.assertFalse(config.smart_quotes) self.assertTrue(config.auto_prefix) self.assertEqual(config.encoding, "cp-1234") @patch("builtins.print") def test_malformed_file(self, _): config = Configuration() self.prepare("smart_quotes: false\n\nencoding") self.assertRaises(SystemExit, config.update_from_file, self.fn) @patch("builtins.print") def test_unrecognized_option(self, a_print): config = Configuration() self.prepare("smart_quotes: false\n\nauto_magog: false") self.assertRaises(SystemExit, config.update_from_file, self.fn) self.assertIn("auto_magog", a_print.call_args[0][0]) @patch("builtins.print") def test_invalid_type(self, a_print): # At the time of writing, Configuration had only bool and str settings, # which can never fail on conversion. To reach that code in test, we # imagine setting that can # Data classes do not support inheritance, so we patch the tested method # into a new class. @dataclasses.dataclass class ConfigurationWithInt: encoding: str = "ascii" foo: int = 42 update_from_file = Configuration.update_from_file config = ConfigurationWithInt() self.prepare("foo: bar") self.assertRaises(SystemExit, config.update_from_file, self.fn) self.assertIn("foo", a_print.call_args[0][0]) @patch("builtins.print") def test_static_files(self, _): config = Configuration() self.assertEqual(len(config.static_files), 0) self.prepare("static-files: static_files_lan") with patch("os.path.exists", lambda x: "test.yaml" in x): self.assertRaises(SystemExit, config.update_from_file, self.fn) with patch("os.path.exists", lambda _: True): config = Configuration() config.update_from_file(self.fn) self.assertEqual(config.static_files, ("static_files_lan", )) self.prepare("static-files:\n" "- static_files_lan\n" "- ban\n" "- pet_podgan\n") with patch("os.path.exists", lambda x: "test.yaml" in x or "lan" in x): self.assertRaises(SystemExit, config.update_from_file, self.fn) with patch("os.path.exists", lambda x: "test.yaml" in x or "an" in x): config = Configuration() config.update_from_file(self.fn) self.assertEqual(config.static_files, ("static_files_lan", "ban", "pet_podgan")) def test_exclude(self): config = Configuration() self.assertTrue(config.exclude_re.search("dir/tests/test_something.py")) self.prepare("exclude-pattern: ba?_m") config.update_from_file(self.fn) self.assertFalse(config.exclude_re.search("dir/tests/test_something.py")) self.prepare("exclude-pattern: ") config.update_from_file(self.fn) self.assertIsNone(config.exclude_re) config.set_exclude_pattern("tests/test_") self.assertTrue(config.exclude_re.search("dir/tests/test_something.py")) config.set_exclude_pattern("") self.assertIsNone(config.exclude_re) def test_languages(self): self.prepare(""" languages: si: name: Slovenščina international-name: Slovenian auto-import: from orangecanvas.utils.localization.si import plsi en: name: English original: true ua: international-name: Ukrainian auto-import: import grain name: Українська auto-import: from orangecanvas.utils.localization import pl """) config = Configuration() with patch("os.path.exists", lambda path: not os.path.join("si", "static") in path): config.update_from_file(self.fn) # Language definitions are correct self.assertEqual( config.languages, {'en': LanguageDef(name='English', international_name='English', is_original=True), 'si': LanguageDef(name='Slovenščina', international_name='Slovenian', is_original=False), 'ua': LanguageDef(name='Українська', international_name='Ukrainian', is_original=False)}) # Original language is first self.assertTrue(next(iter(config.languages.values())).is_original) # Auto-imports are correct self.assertEqual( set(config.auto_import), {'from orangecanvas.utils.localization.si import plsi', 'import grain', 'from orangecanvas.utils.localization import pl'}) # Base dir is set correctly base_dir, _ = os.path.split(self.fn) self.assertEqual(config.base_dir, base_dir) # Static files are correct self.assertEqual( set(config.static_files), {os.path.join(base_dir, "en", "static"), os.path.join(base_dir, "ua", "static")} ) @patch("builtins.print") def test_languages_unknown_option(self, a_print): self.prepare(""" languages: si: name: Slovenščina encoding: utf-8 foo: bar en: name: English original: true """) with patch("os.path.exists", lambda _: True): config = Configuration() self.assertRaises(SystemExit, config.update_from_file, self.fn) self.assertEqual( a_print.call_args[0][0], "Unknown options for language 'si': encoding, foo") @patch("builtins.print") def test_languages_missing_name(self, a_print): self.prepare(""" languages: si: name: Slovenščina en: original: true """) with patch("os.path.exists", lambda _: True): config = Configuration() self.assertRaises(SystemExit, config.update_from_file, self.fn) self.assertEqual( a_print.call_args[0][0], "Language 'en' is missing a 'name' option") @patch("builtins.print") def test_languages_missing_directory(self, a_print): self.prepare(""" languages: si: name: Slovenščina foo-bar-langa: name: fubar en: original: true """) with patch("os.path.exists", lambda path: "foo-bar-lang" not in path): config = Configuration() self.assertRaises(SystemExit, config.update_from_file, self.fn) self.assertIn( "Directory for language 'foo-bar-langa' is missing", a_print.call_args[0][0]) @patch("builtins.print") def test_no_original_language(self, a_print): self.prepare(""" languages: si: name: Slovenščina en: name: English """) with patch("os.path.exists", lambda _: True): config = Configuration() self.assertRaises(SystemExit, config.update_from_file, self.fn) self.assertEqual( "Original language is not defined", a_print.call_args[0][0]) if __name__ == "__main__": unittest.main() trubar-0.3.4/trubar/tests/test_jaml.py000066400000000000000000000255751470120047100200240ustar00rootroot00000000000000import unittest from unittest.mock import patch from trubar import jaml from trubar.messages import MsgNode class JamlReaderTest(unittest.TestCase): def test_read(self): text = """ # jaml file class `A`: foo: bar boo: null # This function contains nothing but another function def `f`: def `g`: # I think that vaz is just # mistyped baz vaz: true baz: false "quoted {dict:03}": "v narekovajih {slovar:04}" 'quoted {dict:05}': 'v narekovajih {slovar:04}' 'quoted {dict:06}': v narekovajih {slovar:04} "This is so: so": to je tako: tako # Yet another class class `B`: nothing: nič """ msgs = jaml.read(text) self.assertEqual( msgs, {'class `A`': MsgNode(comments=["# jaml file"], value={ 'foo': MsgNode(value='bar'), 'boo': MsgNode(value=None), 'def `f`': MsgNode( comments=['# This function contains nothing ' 'but another function'], value={ 'def `g`': MsgNode(value={ 'vaz': MsgNode( comments=['# I think that vaz is just', '# mistyped baz'], value=True), 'baz': MsgNode(value=False), 'quoted {dict:03}': MsgNode( value='v narekovajih {slovar:04}'), 'quoted {dict:05}': MsgNode( value='v narekovajih {slovar:04}'), 'quoted {dict:06}': MsgNode( value='v narekovajih {slovar:04}'), 'This is so: so': MsgNode( value='to je tako: tako') }) } ) }), 'class `B`': MsgNode( comments=['# Yet another class'], value={'nothing': MsgNode(value='nič') }) }) def test_read_empty_file(self): self.assertRaisesRegex(jaml.JamlError, "unexpected end", jaml.read, "") def test_read_quoted_blocks(self): self.assertEqual(jaml.read('''a/b.py: def `f`: "a b c": abc abc: " a ''' + " " * 5 + ''' bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb " foo: false def: "a bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" x/y.py: null '''), {"a/b.py": MsgNode( value={"def `f`": MsgNode({ "a\nb\nc": MsgNode("abc"), "abc": MsgNode( "\n a\n \n " + "b" * 100 + "\n"), "foo": MsgNode(False), "def": MsgNode("a\n" + "b" * 100), })}), "x/y.py": MsgNode(None) } ) def test_read_quotes_in_values(self): text = ''' foo1: "bar" foo2: """bar" foo4: "ba""r" foo5: "bar""" foo7: 'bar' foo8: 'ba''r' foo9: 'ba"r' foo12: "bar''" ''' msgs = jaml.read(text) self.assertEqual( [node.value for node in msgs.values()], ["bar", "\"bar", "ba\"r", "bar\"", "bar", "ba'r", 'ba"r', "bar''"]) def test_read_quotes_in_keys(self): text = """ 1bar": foo2 "2bar": foo3 "3ba""r": foo4 4bar': foo6 '5bar': foo7 '6ba''r': foo8 "7ba""r": foo12 '8ba''r': foo13 """ msgs = jaml.read(text) self.assertEqual( list(msgs), ['1bar"', '2bar', '3ba"r', "4bar'", "5bar", "6ba'r", '7ba"r', "8ba'r"]) def test_read_colons_in_keys(self): text = """ bar:baz: foo1 bar: baz: foo2 "bar:baz: boz": foo3 "bar"": baz: boz": foo4 """ msgs = jaml.read(text) self.assertEqual( list(msgs), ["bar:baz", "bar", "bar:baz: boz", "bar\": baz: boz"] ) def test_stray_comments(self): self.assertRaisesRegex(jaml.JamlError, "5", jaml.read, """ class `A`: def `f`: sth: sth # stray comment def `g`: sth:stg """) self.assertRaisesRegex(jaml.JamlError, "4", jaml.read, """ class `A`: def `f`: # stray comment""") self.assertRaisesRegex(jaml.JamlError, "4", jaml.read, """ class `A`: def `f`: # stray comment """) self.assertRaisesRegex(jaml.JamlError, "4", jaml.read, """ class `A`: def `f`: # stray comment """) def test_indentation_errors(self): # This function checks for exact error messages. While this is not # a good practice in general, it makes these tests more readable self.assertRaisesRegex( jaml.JamlError, "Line 4: unexpected indent", jaml.read, """ abc: def: fgh ghi: jkl jkl: mno prs: tuv: bdf""", ) self.assertRaisesRegex( jaml.JamlError, "Line 4: unindent does not match any outer level", jaml.read, """ abc: def: fgh ghi: jkl jkl: mno prs: tuv: bdf""", ) self.assertRaisesRegex( jaml.JamlError, "Line 4: unindent does not match any outer level", jaml.read, """ abc: def: fgh ghi: jkl jkl: mno prs: tuv: bdf""", ) self.assertRaisesRegex( jaml.JamlError, "Line 4: unindent does not match any outer level", jaml.read, """ abc: def: fgh ghi: jkl jkl: mno prs: tuv:""", ) self.assertRaisesRegex( jaml.JamlError, "Line 9: unexpected end of file", jaml.read, """ abc: def: fgh ghi: jkl jkl: mno prs: tuv: """, ) def test_syntax_errors(self): self.assertRaisesRegex( jaml.JamlError, "Line 1: file ends.*", jaml.read, "'''x: y") self.assertRaisesRegex( jaml.JamlError, "Line 1: file ends.*", jaml.read, "'x: y") self.assertRaisesRegex( jaml.JamlError, "Line 2: quoted key must be followed .*", jaml.read, '"x\ny"\na:b') self.assertRaisesRegex( jaml.JamlError, "Line 2: quoted value must be followed .*", jaml.read, 'x: "\na": b') self.assertRaisesRegex( jaml.JamlError, "Line 1: colon at the end of the key should be " "followed by a space or a new line", jaml.read, "x:y") def test_format_errors(self): # This function checks for exact error messages. While this is not # a good practice in general, it makes these tests more readable self.assertRaisesRegex( jaml.JamlError, "Line 3: key followed by colon expected", jaml.read, """ abc: def ghi: jkl jkl: mno prs: tuv: bdf""", ) self.assertRaisesRegex( jaml.JamlError, "Line 4: file ends", jaml.read, """ abc: def: "ghi: jkl jkl: mno prs: tuv: bdf""", ) @patch("trubar.jaml.read") def test_readfile(self, read): jaml.readfile(jaml.__file__) with open(jaml.__file__, encoding="utf-8") as f: read.assert_called_with(f.read()) class JamlDumperTest(unittest.TestCase): def test_dump(self): tree = {"a/b.py": MsgNode(comments=["# a few", "# initial comments"], value={ "def `f`": MsgNode(comments=["# a function!"], value={"foo": MsgNode("bar"), "baz": MsgNode(None, ["# eh"])}), "yada": MsgNode(comments=["# bada", "# boom"], value=True), "": MsgNode(""), }), "class `A`": MsgNode(value=False)} self.assertEqual( jaml.dump(tree), """ # a few # initial comments a/b.py: # a function! def `f`: foo: bar # eh baz: null # bada # boom yada: true '': "" class `A`: false """[1:]) def test_backslashes(self): self.assertEqual(jaml.dump({r"a\nb": MsgNode(r"c\nd")}).strip(), r"a\nb: c\nd") def test_dump_quotes(self): self.assertEqual(jaml.dump({"'foo'": MsgNode("'asdf'")}), """"'foo'": "'asdf'"\n""") self.assertEqual(jaml.dump({'"foo"': MsgNode('"asdf"')}), """'"foo"': '"asdf"'\n""") self.assertEqual(jaml.dump({"'foo": MsgNode("asdf'")}), """\"'foo": asdf'\n""") def test_dump_spaces_in_value(self): self.assertEqual(jaml.dump({"foo": MsgNode("bar ")}), "foo: 'bar '\n") self.assertEqual(jaml.dump({"foo": MsgNode(" bar")}), "foo: ' bar'\n") def test_quoting_keys(self): self.assertEqual(jaml.dump({"| ": MsgNode(True)}), "'| ': true\n") self.assertEqual(jaml.dump({"# ": MsgNode(True)}), "'# ': true\n") self.assertEqual(jaml.dump({" x": MsgNode(True)}), "' x': true\n") self.assertEqual(jaml.dump({"x ": MsgNode(True)}), "'x ': true\n") self.assertEqual(jaml.dump({" x: y": MsgNode(True)}), "' x: y': true\n") self.assertEqual(jaml.dump({"x:y": MsgNode(True)}), "x:y: true\n") def test_dump_blocks(self): tree = {"a/b.py": MsgNode( value={ "def `f`": MsgNode({ "a\nb\nc": MsgNode("abc"), "abc": MsgNode("\n a\n \n " + "b" * 100 + "\n"), "foo": MsgNode(False), "def": MsgNode("a\n" + "b" * 100), })}), "x/y.py": MsgNode(None) } self.assertEqual(jaml.dump(tree), '''a/b.py: def `f`: 'a b c': abc abc: ' a ''' + " " * 5 + ''' bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ' foo: false def: 'a bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' x/y.py: null ''') if __name__ == "__main__": unittest.main() trubar-0.3.4/trubar/tests/test_main.py000066400000000000000000000046551470120047100200210ustar00rootroot00000000000000import os import unittest from unittest.mock import patch from trubar.config import config from trubar.__main__ import check_dir_exists, load_config import trubar.tests.test_module test_module_path = os.path.split(trubar.tests.test_module.__file__)[0] class TestUtils(unittest.TestCase): @patch("builtins.print") def test_check_dir_exists(self, print_): check_dir_exists(test_module_path) print_.assert_not_called() self.assertRaises( SystemExit, check_dir_exists, os.path.join(test_module_path, "__init__.py")) self.assertIn("not a directory", print_.call_args[0][0]) self.assertRaises(SystemExit, check_dir_exists, "no_such_path") self.assertNotIn("not a directory", print_.call_args[0][0]) @patch.object(config, "update_from_file") def test_load_config(self, update): class Args: source = os.path.join("x", "y", "z") conf = "" messages = "" args = Args() # config is given explicitly args.conf = "foo.yaml" load_config(args) update.assert_called_with("foo.yaml") update.reset_mock() args.conf = "" # .trubarconfig.yaml has precedence over trubar-config.yaml with patch("os.path.exists", lambda x: os.path.split(x)[-1] in ("trubar-config.yaml", ".trubarconfig.yaml")): load_config(args) update.assert_called_with(".trubarconfig.yaml") # ... but trubar-config.yaml is loaded when there is no trubarconfig.yaml with patch("os.path.exists", lambda x: os.path.split(x)[-1] == "trubar-config.yaml"): load_config(args) update.assert_called_with("trubar-config.yaml") # Load from source directory confile = os.path.join(args.source, ".trubarconfig.yaml") with patch("os.path.exists", lambda x: x == confile): load_config(args) update.assert_called_with(confile) # Messages directory has precedence over source mess_dir = os.path.join("t", "u", "v") args.messages = os.path.join(mess_dir, "mess.yaml") confile = os.path.join(mess_dir, ".trubarconfig.yaml") with patch("os.path.exists", lambda x: x == confile): load_config(args) update.assert_called_with(confile) if __name__ == "__main__": unittest.main() trubar-0.3.4/trubar/tests/test_messages.py000066400000000000000000000145151470120047100207000ustar00rootroot00000000000000import io from contextlib import redirect_stdout import unittest from unittest.mock import patch from trubar import messages from trubar.messages import \ load, dump, dict_to_msg_nodes, dict_from_msg_nodes, MsgNode from trubar.tests import TestBase, yamlized class TestUtils(TestBase): def test_load_yaml(self): fn = self.prepare_file("x.yaml", """ class `A`: foo: bar boo: null def `f`: def `g`: vaz: true baz: false "quoted {dict:03}": "v narekovajih {slovar:04}" 'quoted {dict:05}': 'v narekovajih {slovar:04}' class `B`: nothing: nič """) msgs = load(fn) self.assertEqual( msgs, {'class `A`': MsgNode(value={ 'foo': MsgNode(value='bar'), 'boo': MsgNode(value=None), 'def `f`': MsgNode(value={ 'def `g`': MsgNode(value={ 'vaz': MsgNode(value=True), 'baz': MsgNode(value=False), 'quoted {dict:03}': MsgNode( value='v narekovajih {slovar:04}'), 'quoted {dict:05}': MsgNode( value='v narekovajih {slovar:04}') }) }) }), 'class `B`': MsgNode(value={ 'nothing': MsgNode(value='nič') }) }) def test_load_jaml(self): fn = self.prepare_file("x.jaml", """ class `A`: foo: bar: baz """) msgs = load(fn) self.assertEqual( msgs, {'class `A`': MsgNode(value={ 'foo': MsgNode(value='bar: baz') })}) def test_loader_graceful_exit_on_error(self): fn = self.prepare_file("x.jaml", """ class `A`: asdf foo: bar: baz """) with io.StringIO() as buf, redirect_stdout(buf): self.assertRaises(SystemExit, load, fn) self.assertIn("rror in", buf.getvalue()) with io.StringIO() as buf, redirect_stdout(buf): self.assertRaises(SystemExit, load, "no such file") self.assertIn("not found", buf.getvalue()) @patch("builtins.print") def test_loader_verifies_sanity(self, a_print): fn = self.prepare_file("x.jaml", """ class `A`: def `foo`: bar """) self.assertRaises(SystemExit, load, fn) a_print.assert_called() def test_check_sanity(self): # unexpected namespace check_sanity = yamlized(messages.check_sanity) with io.StringIO() as buf, redirect_stdout(buf): self.assertFalse(check_sanity( {"a": "b", "def `f`": {"x": {"y": "z"}}})) self.assertEqual( buf.getvalue(), "def `f`/x: Unexpectedly a namespace\n") # def is not a namespace with io.StringIO() as buf, redirect_stdout(buf): self.assertFalse(check_sanity( {"module": {"a": "b", "def `f`": {"def `x`": "y"}}})) self.assertEqual( buf.getvalue(), "module/def `f`/def `x`: Unexpectedly not a namespace\n") # class is not a namespace with io.StringIO() as buf, redirect_stdout(buf): self.assertFalse(check_sanity( {"module": {"a": "b", "def `f`": {"class `x`": "y"}}})) self.assertEqual( buf.getvalue(), "module/def `f`/class `x`: Unexpectedly not a namespace\n") # everything OK with io.StringIO() as buf, redirect_stdout(buf): self.assertTrue(check_sanity( {"module": {"a": "b", "def `f`": {"class `x`": {"z": "t"}}}})) self.assertEqual( buf.getvalue(), "") # check entire structure, even when there are problems with io.StringIO() as buf, redirect_stdout(buf): self.assertFalse(check_sanity( {"module1": { "a": "b", "def `f`": "t", "class `x`": { "z": "t", "m": {"x": False} }, "t": "o"}, "module2": { "def `g`": None, "x": "y"}, "module3": { "a": "b"} }, "somefile")) self.assertEqual( buf.getvalue(), """Errors in somefile: module1/def `f`: Unexpectedly not a namespace module1/class `x`/m: Unexpectedly a namespace module2/def `g`: Unexpectedly not a namespace """) def test_dict_msg_nodes_conversion(self): msg_dict = { "module1": { "a": "b", "class `x`": { "z": None, "m": {"x": False} }, "t": True}, "module2": { "x": "y"}, } msg_nodes = { 'module1': MsgNode(value={ 'a': MsgNode(value='b'), 'class `x`': MsgNode(value={ 'z': MsgNode(value=None), 'm': MsgNode(value={ 'x': MsgNode(value=False)}, )}), 't': MsgNode(value=True)}, ), 'module2': MsgNode(value={ 'x': MsgNode(value='y')}, )} self.assertEqual(dict_to_msg_nodes(msg_dict), msg_nodes) self.assertEqual(dict_from_msg_nodes(msg_nodes), msg_dict) self.assertEqual( dict_from_msg_nodes({"x": MsgNode("foo", ["bar", "baz"])}), {"x": "foo"}) @patch("builtins.open") @patch("yaml.dump") @patch("trubar.jaml.dump") def test_dump(self, jaml_dump, yaml_dump, _): msgdict = {"x": MsgNode("foo", ["bar", "baz"])} dump(msgdict, "x.jaml") jaml_dump.assert_called_with(msgdict) jaml_dump.reset_mock() yaml_dump.assert_not_called() msgdict = {"x": MsgNode("foo", ["bar", "baz"])} dump(msgdict, "x.yaml") self.assertEqual(yaml_dump.call_args[0][0], dict_from_msg_nodes(msgdict)) jaml_dump.assert_not_called() if __name__ == "__main__": unittest.main() trubar-0.3.4/trubar/tests/test_module/000077500000000000000000000000001470120047100177765ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/test_module/__init__.py000066400000000000000000000001751470120047100221120ustar00rootroot00000000000000class Future: about_to_happen = "All those moments will be lost in time, like tears in rain..." time = "Time to die."trubar-0.3.4/trubar/tests/test_module/bar_module/000077500000000000000000000000001470120047100221075ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/test_module/bar_module/__init__.py000066400000000000000000000000671470120047100242230ustar00rootroot00000000000000x = "Attack ships on fire off the shoulder of Orion..."trubar-0.3.4/trubar/tests/test_module/bar_module/foo_module/000077500000000000000000000000001470120047100242375ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/test_module/bar_module/foo_module/__init__.py000066400000000000000000000000651470120047100263510ustar00rootroot00000000000000t = "I've seen things you people wouldn't believe..."trubar-0.3.4/trubar/tests/test_module/baz_module/000077500000000000000000000000001470120047100221175ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/test_module/baz_module/__init__.py000066400000000000000000000001261470120047100242270ustar00rootroot00000000000000def f(): return "I watched C-beams glitter in the dark near the Tannhäuser Gate."trubar-0.3.4/trubar/tests/test_module/baz_module/not-python.js000066400000000000000000000000261470120047100245720ustar00rootroot00000000000000x = "don't touch this"trubar-0.3.4/trubar/tests/test_module_2/000077500000000000000000000000001470120047100202175ustar00rootroot00000000000000trubar-0.3.4/trubar/tests/test_module_2/__init__.py000066400000000000000000000001301470120047100223220ustar00rootroot00000000000000 """Doc string a bit later""" def f(x): a = "not a docstring" "useless string"trubar-0.3.4/trubar/tests/test_module_2/submodule.py000066400000000000000000000001511470120047100225650ustar00rootroot00000000000000"""docstring""" def f(x): "docstring" a = "not a docstring" def g(x): """Also docstring""" trubar-0.3.4/trubar/tests/test_utils.py000066400000000000000000000225411470120047100202270ustar00rootroot00000000000000import os from pathlib import PureWindowsPath import unittest from unittest.mock import patch, Mock from trubar.utils import \ walk_files, check_any_files, unique_name, dump_removed, make_list from trubar.config import config import trubar.tests.test_module test_module_path = os.path.split(trubar.tests.test_module.__file__)[0] class UtilsTest(unittest.TestCase): def test_walk_files(self): tmp = test_module_path old_pattern = config.exclude_pattern try: config.set_exclude_pattern("") self.assertEqual( set(walk_files(tmp, select=True)), {('bar_module/__init__.py', f'{tmp}/bar_module/__init__.py'), ('bar_module/foo_module/__init__.py', f'{tmp}/bar_module/foo_module/__init__.py'), ('baz_module/__init__.py', f'{tmp}/baz_module/__init__.py'), ('__init__.py', f'{tmp}/__init__.py')} ) old_path = os.getcwd() try: os.chdir(tmp) self.assertEqual( set(walk_files(".", select=True)), {('bar_module/__init__.py', './bar_module/__init__.py'), ('bar_module/foo_module/__init__.py', './bar_module/foo_module/__init__.py'), ('baz_module/__init__.py', './baz_module/__init__.py'), ('__init__.py', './__init__.py')} ) self.assertEqual( set(walk_files(".", "bar", select=True)), {('bar_module/__init__.py', './bar_module/__init__.py'), ('bar_module/foo_module/__init__.py', './bar_module/foo_module/__init__.py')} ) finally: os.chdir(old_path) self.assertEqual( set(walk_files(tmp, select=False)), {('bar_module/__init__.py', f'{tmp}/bar_module/__init__.py'), ('bar_module/foo_module/__init__.py', f'{tmp}/bar_module/foo_module/__init__.py'), ('baz_module/__init__.py', f'{tmp}/baz_module/__init__.py'), ('__init__.py', f'{tmp}/__init__.py'), ('baz_module/not-python.js', f'{tmp}/baz_module/not-python.js')} ) config.set_exclude_pattern("b?r_") self.assertEqual( set(walk_files(tmp, select=False)), {('bar_module/__init__.py', f'{tmp}/bar_module/__init__.py'), ('bar_module/foo_module/__init__.py', f'{tmp}/bar_module/foo_module/__init__.py'), ('baz_module/__init__.py', f'{tmp}/baz_module/__init__.py'), ('__init__.py', f'{tmp}/__init__.py'), ('baz_module/not-python.js', f'{tmp}/baz_module/not-python.js')} ) self.assertEqual( set(walk_files(tmp, select=True)), {('baz_module/__init__.py', f'{tmp}/baz_module/__init__.py'), ('__init__.py', f'{tmp}/__init__.py')} ) finally: config.set_exclude_pattern(old_pattern) @patch("os.walk", return_value=[(r"c:\foo\bar\ann\bert", None, ["cecil.py", "dan.txt", "emily.py"])] ) @patch("os.path.join", new=lambda *c: "\\".join(c)) @patch("trubar.utils.PurePath", new=PureWindowsPath) def test_walk_backslashes_on_windows(self, _): self.assertEqual( list(walk_files(r"c:\foo\bar", select=False)), [("ann/bert/cecil.py", r"c:\foo\bar\ann\bert\cecil.py"), ("ann/bert/dan.txt", r"c:\foo\bar\ann\bert\dan.txt"), ("ann/bert/emily.py", r"c:\foo\bar\ann\bert\emily.py")] ) self.assertEqual( list(walk_files(r"c:\foo\bar", "l", select=False)), [("ann/bert/cecil.py", r"c:\foo\bar\ann\bert\cecil.py"), ("ann/bert/emily.py", r"c:\foo\bar\ann\bert\emily.py")]) # Don't match pattern in path self.assertEqual( list(walk_files(r"c:\foo\bar", "o", select=False)), []) @patch("sys.exit") @patch("builtins.print") def test_check_any_files(self, print_, exit_): keys = "a/x.py a/y.py a/b/x.py a/b/z.py".split() translations = dict.fromkeys(keys) with patch("trubar.utils.walk_files", Mock(return_value=[(k, k) for k in keys])): check_any_files(set(translations), "foo/bar") exit_.assert_not_called() print_.assert_not_called() with patch("trubar.utils.walk_files", Mock(return_value=[("t/x/" + k, k) for k in keys])): check_any_files(set(translations), "foo/bar") exit_.assert_called() print_.assert_called() msg = print_.call_args[0][0] self.assertIn("-s foo/bar/t/x", msg) print_.reset_mock() exit_.reset_mock() home = os.path.expanduser("~/foo/bar") check_any_files(set(translations), home) exit_.assert_called() msg = print_.call_args[0][0] self.assertIn("-s ~/foo/bar/t/x", msg) print_.reset_mock() exit_.reset_mock() with patch("sys.platform", "win32"): check_any_files(set(translations), home) exit_.assert_called() print_.assert_called() msg = print_.call_args[0][0] print_.assert_called() self.assertIn(f"-s {home}/t/x", msg) print_.reset_mock() exit_.reset_mock() keys = "a/x.py a/y.py a/b/x.py a/b/z.py a/b/u.py".split() with patch("trubar.utils.walk_files", Mock(return_value=[(k, k) for k in keys])): check_any_files({"x.py", "z.py", "u.py"}, "foo") exit_.assert_called() print_.assert_called() msg = print_.call_args[0][0] self.assertIn("-s foo/a/b", msg) print_.reset_mock() exit_.reset_mock() keys = "a/x.py a/y.py a/b/x.py a/b/z.py".split() with patch("trubar.utils.walk_files", Mock(return_value=[(k, k) for k in keys])): check_any_files({"x.py", "z.py", "u.py"}, "foo") exit_.assert_called() print_.assert_called() msg = print_.call_args[0][0] self.assertNotIn("-s foo/a/b", msg) print_.reset_mock() exit_.reset_mock() def test_unique_name(self): with patch("os.path.exists", return_value=False): self.assertEqual(unique_name("some name.yaml"), "some name.yaml") with patch("os.path.exists", return_value=True): with patch("os.listdir", return_value=["some name.yaml"]) as listdir: self.assertEqual(unique_name("some name.yaml"), "some name (1).yaml") listdir.assert_called_with(".") self.assertEqual(unique_name("abc/def/some name.yaml"), "abc/def/some name (1).yaml") listdir.assert_called_with("abc/def") with patch("os.listdir", return_value=["some name.yaml", "some name (1).yaml", "some name (2).yaml", "non sequitur.yaml", ]) as listdir: self.assertEqual(unique_name("some name.yaml"), "some name (3).yaml") listdir.assert_called_with(".") with patch("os.listdir", return_value=["some name.yaml", "some name (1).yaml", "non sequitur.yaml", "some name (4).yaml", ]) as listdir: self.assertEqual(unique_name("some name.yaml"), "some name (5).yaml") listdir.assert_called_with(".") @patch("trubar.utils.dump") def test_dump_removed(self, mock_dump): dump_removed({}, "removed.yaml", "abc/def/x.yaml") mock_dump.assert_not_called() msgs = Mock() dump_removed(msgs, "removed.yaml", "abc/def/x.yaml") mock_dump.assert_called_with(msgs, "removed.yaml") dump_removed(msgs, None, "abc/def/xyz.jaml") mock_dump.assert_called_with(msgs, "abc/def/removed-from-xyz.jaml") def test_make_list(self): self.assertEqual(make_list(["a"]), "a") self.assertEqual(make_list(["a", "b"]), "a and b") self.assertEqual(make_list(["a", "b", "c"]), "a, b and c") self.assertEqual(make_list(["a"], "use"), "a uses") self.assertEqual(make_list(["a", "b", "c"], "use"), "a, b and c use") if __name__ == "__main__": unittest.main() trubar-0.3.4/trubar/utils.py000066400000000000000000000057411470120047100160310ustar00rootroot00000000000000import re import os import sys from pathlib import PurePath from typing import Iterator, Tuple, Optional, Set, List from trubar.config import config from trubar.messages import MsgDict, dump def walk_files(path: str, pattern: str = "", *, select: bool ) -> Iterator[Tuple[str, str]]: path = os.path.normpath(path) for dirpath, _, files in sorted(os.walk(path)): for name in sorted(files): if name.endswith(".pyc") \ or select and not name.endswith(".py"): continue name = os.path.join(dirpath, name) keyname = PurePath(name[len(path) + 1:]).as_posix() if pattern in keyname and \ not (select and config.exclude_re and config.exclude_re.search(keyname)): yield keyname, name def check_any_files(trans_files: Set[str], path: str): source_keys = {n for n, _ in walk_files(path, "", select=True)} if not trans_files or source_keys & trans_files: return suggestion = "" best_matched = 2 # Require at least three matches to make a suggestion tried = set() for fname in source_keys: start = "" for part in fname.split("/")[:3]: start += part + "/" if start in tried: continue tried.add(start) matched = len(source_keys & {start + k for k in trans_files}) if matched > best_matched: best_matched = matched suggestion = start if suggestion: suggestion = os.path.join(path, suggestion[:-1]) if sys.platform != "win32": home = os.path.expanduser("~") if suggestion.startswith(home): suggestion = "~/" + suggestion[len(home) + 1:] suggestion = f"; try -s {suggestion} instead" print("Paths in translations do not match any existing files.\n" f"One reason may be an incorrect source path{suggestion}.") sys.exit(5) def unique_name(name: str) -> str: if not os.path.exists(name): return name path, name = os.path.split(name) base, ext = os.path.splitext(name) pattern = fr"{re.escape(base)} \((\d+)\){ext}" existing = (re.match(pattern, fname) for fname in os.listdir(path or ".")) ver = 1 + max((int(mo.group(1)) for mo in existing if mo), default=0) return os.path.join(path, f"{base} ({ver}){ext}") def dump_removed(removed: MsgDict, removed_name: Optional[str], name: str) -> None: if not removed: return if not removed_name: path, name = os.path.split(name) removed_name = os.path.join(path, "removed-from-" + name) removed_name = unique_name(removed_name) dump(removed, removed_name) def make_list(s: List[str], verb: Optional[str] = None): verb = "" if verb is None else " " + verb + "s" * (len(s) == 1) if len(s) == 1: return s[0] + verb else: return ", ".join(s[:-1]) + " and " + s[-1] + verb