Beispiel #1
0
 def _show_collect_results(self):
     """Display results of collecting source strings from files."""
     total_strings = sum([x[1] for x in self.stats['strings']])
     Color.echo('Processed [warn]{}[end] files and found [warn]{}[end] '
                'translatable strings in [warn]{}[end] of them.'.format(
                    self.stats['processed_files'], total_strings,
                    len(self.stats)))
Beispiel #2
0
    def push_strings(self):
        """Push strings to the CDS."""
        total = len(self.string_collection.strings)
        if total == 0:
            Color.echo('[warn]There are no strings to push to Transifex.[end]')
            return

        Color.echo('Pushing [warn]{}[end] unique translatable strings '
                   'to Transifex...'.format(total))
        status_code, response_content = tx.push_source_strings(
            self.string_collection.strings.values(), self.purge)
        self._show_push_results(status_code, response_content)
Beispiel #3
0
    def _show_push_results(self, status_code, response_content):
        """Display results of pushing the source strings to CDS.

        :param int status_code: the HTTP status code
        :param dict response_content: the content of the response
        """
        try:
            if 200 <= status_code < 300:
                # {"created":0,"updated":5,"skipped":1,"deleted":0,"failed":0,"errors":[]}
                created = response_content.get('created')
                updated = response_content.get('updated')
                skipped = response_content.get('skipped')
                deleted = response_content.get('deleted')
                failed = response_content.get('failed')
                errors = response_content.get('errors', [])
                Color.echo(
                    '[green]\nSuccessfully pushed strings to Transifex.[end]\n'
                    '[high]Status:[end] [warn]{code}[end]\n'
                    '[high]Created strings:[end] [warn]{created}[end]\n'
                    '[high]Updated strings:[end] [warn]{updated}[end]\n'
                    '[high]Skipped strings:[end] [warn]{skipped}[end]\n'
                    '[high]Deleted strings:[end] [warn]{deleted}[end]\n'
                    '[high]Failed strings:[end] [warn]{failed}[end]\n'
                    '[high]Errors:[end] {errors}[end]\n'.format(
                        code=status_code,
                        created=created,
                        updated=updated,
                        skipped=skipped,
                        deleted=deleted,
                        failed=failed,
                        errors='\n'.join(errors)))

            else:
                message = response_content.get('message')
                details = response_content.get('details')
                Color.echo(
                    '[error]\nCould not push strings to Transifex.[end]\n'
                    '[high]Status:[end] [warn]{code}[end]\n'
                    '[high]Message:[end] [warn]{message}[end]\n'
                    '[high]Details:[end] [warn]{details}[end]\n'.format(
                        code=status_code,
                        message=message,
                        details=json.dumps(details, indent=4),
                    ))
        except Exception:
            self.output('(Error while printing formatted report, '
                        'falling back to raw format)\n'
                        'Status: {code}\n'
                        'Content: {content}'.format(
                            code=status_code,
                            content=response_content,
                        ))
Beispiel #4
0
 def _show_collect_results(self):
     """Display results of collecting source strings from files."""
     total_strings = sum([x[1] for x in self.stats['strings']])
     Color.echo('Processed [warn]{}[end] files and found [warn]{}[end] '
                'translatable strings in [warn]{}[end] of them.'.format(
                    self.stats['processed_files'], total_strings,
                    len(self.stats)))
     if self.verbose_output:
         file_list = '\n'.join([
             u'[pink]{}.[end] {}'.format((cnt + 1), string_repr(x)) for cnt,
             x in enumerate(self.string_collection.strings.values())
         ])
         Color.echo(file_list)
Beispiel #5
0
    def push_strings(self):
        """Push strings to the CDS."""
        total = len(self.string_collection.strings)
        if total == 0:
            Color.echo('[warn]There are no strings to push to Transifex.[end]')
            return

        Color.echo('Pushing [warn]{}[end] unique translatable strings '
                   'to Transifex...'.format(total))
        status_code, response_content = tx.push_source_strings(
            self.string_collection.strings.values(), self.purge)

        if self.no_wait:
            Color.echo('Queued')
            return

        job_url = response_content['data']['links']['job']
        status = 'starting'
        while status in ['starting', 'pending', 'processing']:
            time.sleep(1)
            status_code, response_content = tx.get_push_status(job_url)
            new_status = response_content['data']['status']
            if new_status != status:
                status = new_status
                if status == 'pending':
                    sys.stdout.write('In queue...')
                elif status == 'processing':
                    sys.stdout.write('Processing...')
            else:
                sys.stdout.write('.')
            sys.stdout.flush()

        Color.echo('')
        self._show_push_results(status_code, response_content)
def test(template_str, context_dict=None, autoescape=True, i=''):
    """ Use the django templating engine to run a test.

        Arguments:

        :param template_str: The template to render
        :param context_dict: The context to render the template against
        :param autoescape:   Pretend the django templating engine was setup
                             with autoescape or not (in most real use-cases, it
                             will have been set up with autoescape=True)
        :param i:            Prepend the output with this in order to help
                             distinguish tests when multiple are run

        Information about (auto)escaping in django:
        https://docs.djangoproject.com/en/3.0/ref/templates/language/#automatic-html-escaping  # noqa
    """

    if context_dict is None:
        context_dict = {}
    context = Context(dict(context_dict), autoescape=autoescape)
    template = ('{% load transifex %}' + template_str)
    try:
        result = Template(template).render(context)
    except Exception:
        print(template_str, context_dict, autoescape)
        raise
    Color.echo("[warn]{i:4}[end]. [cyan]Template[end]:    {template}".
               format(i=i, template=template_str))
    Color.echo("      [cyan]Context[end]:     {context}".
               format(context=context_dict))
    Color.echo("      [cyan]Autoescape[end]:  {autoescape}".
               format(autoescape=autoescape))
    Color.echo("      [cyan]Result[end]:      [green]{result}[end]".
               format(result=result))
    print()
def migrate_text(text, migrator_func):
    """Convert the given text from the original framework to Native syntax.

    Supports both HTML/template syntax and Python/gettext syntax.
    Prints out the result in the console.

    :param unicode text: the text to migrate to Native syntax
    :param callable migrator_func: a Callable[unicode] -> FileMigration object
        that converts syntax to Transifex Native; provided externally
        so that it can support any Python framework (e.g. Django)
    """
    Color.echo('[high]Original syntax:[end]\n[red]{}[end]'.format(text))
    file_migration = migrator_func(text)
    Color.echo('\n[high]Transifex Native syntax:[end]\n[green]{}[end]'.format(
        file_migration.compile()))
Beispiel #8
0
    def mark_file(self, file_migration):
        if not file_migration.low_confidence_strings:
            return False

        first_string_migration = file_migration.strings[0]
        if MARK_PROOFREAD_FILE in first_string_migration.new:
            return False

        mark_string(
            first_string_migration,
            self._comment_format,
            MARK_PROOFREAD_FILE,
        )
        Color.echo('📝 File automatically marked for proofreading')
        return True
Beispiel #9
0
    def collect_strings(self):
        """Search all related files, collect and store translatable strings.

        Stores found strings in `self.string_collection`.
        """
        Color.echo(
            '[high]\n'
            '##############################################################\n'
            'Transifex Native: Parsing files to detect translatable content'
            '[end]')
        files = self._find_files('.', 'push')
        for f in files:
            extracted_strings = self._extract_strings(f)
            self.string_collection.extend(extracted_strings)
            self.stats['processed_files'] += 1
            if extracted_strings and len(extracted_strings):
                self.stats['strings'].append((f.file, len(extracted_strings)))

        # Append optional CLI tags
        if self.append_tags:
            extra_tags = [x.strip() for x in self.append_tags.split(',')]
            for key, string in self.string_collection.strings.items():
                new_string_tags = set(string.tags + extra_tags)
                string.meta[consts.KEY_TAGS] = list(new_string_tags)

        # Filter out strings based on tags, i.e. only push strings
        # that contain certain tags or do not contain certain tags
        if self.with_tags_only:
            included_tags = {x.strip() for x in self.with_tags_only.split(',')}
        else:
            included_tags = set()
        if self.without_tags_only:
            excluded_tags = {
                x.strip()
                for x in self.without_tags_only.split(',')
            }
        else:
            excluded_tags = set()

        if included_tags or excluded_tags:
            self.string_collection.update([
                string
                for key, string in self.string_collection.strings.items()
                if included_tags.issubset(set(string.tags))
                and not excluded_tags.intersection(set(string.tags))
            ])
        self._show_collect_results()
Beispiel #10
0
    def handle(self, *args, **options):
        purge = options['purge']

        if purge:
            Color.echo('Purging CDS cache...')
        else:
            Color.echo('Invalidating CDS cache...')

        status_code, response_content = tx.invalidate_cache(purge)

        try:
            if 200 <= status_code < 300:
                # {"data": { "count":0 }}
                count = response_content['data']['count']
                if purge:
                    Color.echo('[green]\nSuccessfully purged CDS cache.[end]\n'
                               '[high]Status:[end] [warn]{code}[end]\n'
                               '[high]Records purged: {count}[end]\n'.format(
                                   code=status_code,
                                   count=count,
                               ))
                else:
                    Color.echo(
                        '[green]\nSuccessfully invalidated CDS cache.[end]\n'
                        '[high]Status:[end] [warn]{code}[end]\n'
                        '[high]Records invalidated: {count}[end]\n'
                        '[high]Note: It might take a few minutes for '
                        'fresh content to be available\n'.format(
                            code=status_code,
                            count=count,
                        ))
            else:
                message = response_content.get('message')
                Color.echo('[error]\nCould not invalidate CDS.[end]\n'
                           '[high]Status:[end] [warn]{code}[end]\n'
                           '[high]Message:[end] [warn]{message}[end]\n'.format(
                               code=status_code,
                               message=message,
                           ))
        except Exception:
            self.output('(Error while printing formatted report, '
                        'falling back to raw format)\n'
                        'Status: {code}\n'
                        'Content: {content}'.format(
                            code=status_code,
                            content=response_content,
                        ))
Beispiel #11
0
    def collect_strings(self):
        """Search all related files, collect and store translatable strings.

        Stores found strings in `self.string_collection`.
        """
        Color.echo(
            '[high]\n'
            '##############################################################\n'
            'Transifex Native: Parsing files to detect translatable content'
            '[end]')
        files = self._find_files('.', 'push')
        for f in files:
            extracted = self._extract_strings(f)
            self.string_collection.extend(extracted)
            self.stats['processed_files'] += 1
            if extracted and len(extracted):
                self.stats['strings'].append((f.file, len(extracted)))
        self._show_collect_results()
Beispiel #12
0
 def print_original_file(file_migration):
     """Print all the lines of the file as it was originally,
     without any highlighting."""
     Color.echo('[prompt]This is the original file[end]')
     Color.echo('[prompt]-------------------------------------[end]')
     print(add_line_prefix(''.join(file_migration.original_content), '', 0))
     Color.echo('[prompt]-------------------------------------[end]')
Beispiel #13
0
    def _safe_save(self, path, content_func, file_type):
        """Attempt to save the string provided by the given callable to the
        given file path, gracefully handling any exception.

        Requires a callable so that it also catches any exceptions raised
        during the generation of the content.

        Usage:
        >>> _safe_save('file/path.html', lambda: content, file_type='Backup')  # noqa
        >>> _safe_save('file/path.html', my_provider.get_content, file_type='Backup')  # noqa

        :param basestring path: the path to save to
        :param callable content_func: a callable that should return the content
            to save in the file
        :param basestring file_type: the type of the file that was saved,
            used to display a more clear message to the user
            e.g. 'backup' or 'original'
        :return: a tuple that shows if the file was saved and the type of
            exception raised if applicable
        :rtype: Tuple[bool, type]
        """
        try:
            with io.open(path, "w", encoding="utf-8") as f:
                f.write(content_func())
                Color.echo('💾️ {} file saved at [file]{}[end]'.format(
                    file_type, path))
                return True, None
        except IOError as e:
            Color.echo('❌ [red]IOError while saving to {} file[end] '
                       '[file]{}[end]: {}'.format(file_type.lower(), path, e))
            return False, type(e)
        except Exception as e:
            Color.echo('❌ [red]Error while saving to {} file[end]'
                       ' [file]{}[end]: {}'.format(file_type.lower(), path, e))
            return False, type(e)
Beispiel #14
0
    def save_file(self, file_migration):
        """Save the new content in the original path, but take a backup first.

        :param FileMigration file_migration: the migration of a whole file
        """
        # Save the original content in a backup file
        backup_filename = file_migration.filename + '.bak'
        success = self._safe_save(
            backup_filename,
            lambda: file_migration.original_content,
            file_type='Backup',
        )

        # If the backup failed, do not modify the original file
        if not success:
            Color.echo('[warn]  -> will not modify the original file '
                       '[file]{}[end]'.format(file_migration.filename))
            return False

        # Save the new content in the original file
        return self._safe_save(file_migration.filename,
                               file_migration.compile,
                               file_type='Original')
Beispiel #15
0
            def add_in_between(txt_range):
                """Add the simple text found between the last migrated node
                and the current node.

                Ensures that the text that should remain untransformed
                is included in the final file.
                """
                if txt_range[0] > last_migrated_char:
                    try:
                        original_str = src[last_migrated_char:txt_range[0]]
                        file_migration.add_string(
                            StringMigration(
                                original=original_str,
                                new=original_str,
                                confidence=Confidence.HIGH,
                            ))
                    except Exception as e:
                        Color.echo(
                            '[error]Error while adding in-between content '
                            'in [file]{}[end]. last_migrated_char={}.'
                            'Error: {}'.format(filename, last_migrated_char,
                                               e))
                        raise
Beispiel #16
0
def yes_no(description, yes_message=None, no_message=None):
    """Prompts the user to reply to a Yes/No question.

    :param basestring description: the message to display before prompting
    :param basestring yes_message: the message to display if user accepts
    :param basestring no_message: the message to display is user declines
    :return: True if the user chose to go through, false otherwise
    :rtype: bool
    """
    while True:
        reply = prompt(
            Color.format('[opt](Y)[end] Yes [opt](N)[end] No'),
            description=description,
            default='N',
        )
        reply = reply.upper()
        if reply == 'Y':
            if yes_message:
                Color.echo('[high]{}[end]'.format(yes_message))
            return True
        elif reply == 'N':
            if no_message:
                Color.echo('[high]{}[end]'.format(no_message))
            return False
Beispiel #17
0
    def print_new_file(file_migration):
        """Print all the lines of the file, highlighting the new chars only."""
        Color.echo('[prompt]This is the final file[end]')
        Color.echo('[prompt]-------------------------------------[end]')

        output = []
        for string_migration in file_migration.strings:
            if not string_migration.modified:
                output.append(Color.format(string_migration.original))
            else:
                output.append(
                    Color.format('[green]{}[end]'.format(
                        string_migration.new)))

        print(add_line_prefix(''.join(output), '', 0))
        Color.echo('[prompt]-------------------------------------[end]')
Beispiel #18
0
    def _prompt_to_start(self, total_files):
        """Prompt the user before starting the migration.

        If the user chooses to not go through with it, sys.exit() is called.

        :param int total_files: the total number of files to migrate
        """
        msg = pluralized(
            'Found [warn]{cnt}[end] file to check for translatable strings.',
            'Found [warn]{cnt}[end] files to check for translatable strings.',
            total_files,
        )
        Color.echo('\n{}'.format(msg))

        if not total_files:
            Color.echo('\n[high]Migration ended.[end]')
            sys.exit(1)

        if (self.options['save_policy'] != NoopSavePolicy.name
                and self.options['review_policy'] == NoopReviewPolicy.name):
            Color.echo(
                '\n[warn]WARNING! The selected configuration will save '
                'all files automatically, without allowing you to do any '
                'reviewing first.[end]')

        while True:
            reply = prompt(
                Color.format('[opt](Y)[end] Yes [opt](N)[end] No'),
                description='Are you sure you want to continue?',
                default='N',
            )
            reply = reply.upper()
            if reply == 'Y':
                return
            elif reply == 'N':
                Color.echo('\n[high]Migration aborted.[end]')
                sys.exit(1)
def fancy_input(text, *choices):
    """ Multiple choice input

        Given arguments ('Which tag do you want to use?',
                         ('t', 't', '{% t ... %}'),
                         ('ut', 'ut', '{% ut ... %}'))

        It will render:

            ===> Which tag do you want to use?
            .... 1. t                 : {% t ... %}
            .... 2. ut                : {% ut ... %}
            .... [examples: "1", "1 3"; empty input for all choices]

        The return value will be a list of "choices", ie the first items in the
        3-tuples.
    """

    print()
    Color.echo("[yel]===>[end] {}".format(text))
    for i, (_, display, example) in enumerate(choices, 1):
        line = "[yel]....[end] [warn]{}[end]. {:18}".format(i, display)
        if example:
            line += ": [cyan]{}[end]".format(example)
        Color.echo(line)

    while True:
        Color.echo('.... [examples: "[warn]1[end]", "[warn]1 3[end]"; '
                   '[warn]empty input[end] for all choices]')
        answer = input("===> ")
        if answer.strip() == "":
            return [choice for choice, _, _ in choices]
        try:
            answer = [int(choice) - 1 for choice in answer.split()]
        except Exception:
            pass
        else:
            if all((0 <= choice < len(choices) for choice in answer)):
                return [choices[choice][0] for choice in answer]
        print("Invalid answer, please try again")
Beispiel #20
0
    def migrate_files(self, files):
        """Search all related files, detect Django i18n translate hooks and
        migrate them to Transifex syntax.

        :param list files: a list of TranslatableFile objects
        """
        files_total = len(files)

        # Ask the user for permission to continue
        self._prompt_to_start(len(files))

        accept_remaining_files = False
        exit_migration = False

        # Loop through each file, migrate, ask for user review if applicable,
        # save to disk if applicable
        for file_cnt, translatable_file in enumerate(files):
            if exit_migration:
                break

            comment_format = self._comment_format(translatable_file.file)
            self.review_policy.set_comment_format(comment_format)
            self.mark_policy.set_comment_format(comment_format)

            Color.echo('\n---- '
                       '[[high]{cnt}[end]/[high]{total}[end]] '
                       'Migrating [file]{path}[end]...'.format(
                           path=translatable_file.path,
                           cnt=file_cnt + 1,
                           total=files_total,
                       ))
            self.stats['processed_files'] += 1
            file_migration = self.file_migrator_func(translatable_file)
            if not file_migration:
                continue

            modified_strings = file_migration.modified_strings
            total_modified = len(modified_strings)
            total_low_confidence = len([
                x for x in modified_strings if x.confidence == Confidence.LOW
            ])
            msg = pluralized(
                '[warn]1[end] [prompt]string was modified[end]',
                '[warn]{cnt}[end] [prompt]strings were modified[end]',
                total_modified,
            )
            Color.echo('{msg}{confidence}'.format(
                msg=msg,
                confidence=(' ([warn]{low}[end] with low confidence)'.format(
                    low=total_low_confidence, )
                            if total_low_confidence else '')))
            if not total_modified:
                continue

            # If the review policy says so, prompt the user for each string
            # If this returns True, it doesn't necessarily mean that the user
            # will be prompted, as the actual review policy may use
            # additional filters, e.g. only prompt for strings with low
            # confidence
            if self.review_policy.should_review_strings():
                reject_remaining_strings = False
                for string_index in range(total_modified):
                    string_migration = modified_strings[string_index]

                    # The rest of the string test_migrations should be reverted
                    # based on the user's choice
                    if reject_remaining_strings:
                        string_migration.revert()

                    # Optionally prompt the user to review the migration
                    else:
                        # Give the user the option to review
                        # May modify `string_migration` in place
                        result = self.review_policy.review_string(
                            string_migration, string_index, total_modified)
                        # The user has chosen to accept the changes
                        # in all remaining strings. Break so that
                        # there will be no more prompts for the rest
                        # of the strings
                        if result == REVIEW_ACCEPT_ALL:
                            break

                        # The user has chosen to reject the changes in all
                        # remaining strings. Set the flag to True, so that
                        # it will revert all changes for the rest strings
                        # in the loop
                        elif result == REVIEW_REJECT_ALL:
                            reject_remaining_strings = True

                        # The user has chosen to exit the migration completely
                        # Break to exit the outer (file) loop
                        elif result == REVIEW_EXIT:
                            exit_migration = True
                            break

            # If the mark policy says so, give it a chance to mark
            # each string for proofread
            if self.mark_policy.should_mark_strings():
                for string_migration in modified_strings:
                    marked = self.mark_policy.mark_string(string_migration)
                    if marked:
                        self.stats['strings_marked'] += 1

            # If the review policy says so, prompt the user for each file
            if accept_remaining_files is False and exit_migration is False:
                result = self.review_policy.review_file(file_migration)

                # The user has chosen to reject all remaining files
                # Break, so that we exit the outer (file) loop
                if result == REVIEW_REJECT_ALL:
                    break

                # The user has chosen to accept all remaining files
                # Set the flag, so that the file review policy won't be used
                # for the remaining file test_migrations
                elif result == REVIEW_ACCEPT_ALL:
                    accept_remaining_files = True

                # The user has chosen to exit the migration completely
                elif result == REVIEW_EXIT:
                    exit_migration = True

            # Skip to the results
            # Break to exit the outer (file) loop
            if exit_migration is True:
                break

            # Give a chance to the mark policy to mark the file for proofread
            marked = self.mark_policy.mark_file(file_migration)
            if marked:
                self.stats['files_marked'] += 1

            # If the save policy says so, save the changes
            if file_migration.modified_strings:
                saved, error_type = self.save_policy.save_file(file_migration)
            else:
                saved, error_type = False, None

            # Update stats
            self.stats['migrations'].append(
                (translatable_file.path, file_migration))
            if saved:
                self.stats['saved'].append(file_migration)
            elif error_type is not None:
                self.stats['errors'].append(file_migration)

        self._show_results(files, self.stats)
Beispiel #21
0
 def show_intro(self):
     """Show an introductory message to help the user understand what
     is going on.
     """
     Color.echo(
         '[high]'
         '\n############################################################'
         '########\n'
         'Running migration from Django i18n syntax to Transifex Native '
         'syntax\n'
         '[end]'
         '\nThis migration is idempotent, so its output should not '
         'change if run'
         '\nmultiple times with the same configuration.')
     Color.echo('\n[high]Configuration:[end]')
     if self.options['path']:
         Color.echo('[opt]Path:[end] [file]{}[end]'.format(
             self.options['path']))
     if self.options['files']:
         Color.echo('[opt]Files:[end]')
         Color.echo('\n'.join(
             [' - [file]{}[end]'.format(x) for x in self.options['files']]))
     Color.echo('[opt]Review policy:[end] [high]{}[end] -> {}'.format(
         self.options['review_policy'],
         REVIEW_POLICY_OPTIONS[self.options['review_policy']],
     ).strip())
     Color.echo('[opt]Save policy:[end] [high]{}[end] -> {}'.format(
         self.options['save_policy'],
         SAVE_POLICY_OPTIONS[self.options['save_policy']],
     ).strip())
     Color.echo('[opt]Mark policy:[end] [high]{}[end] -> {}'.format(
         self.options['mark_policy'],
         MARK_POLICY_OPTIONS[self.options['mark_policy']],
     ).strip())
Beispiel #22
0
    def print_file_with_diff(file_migration):
        """Print all the lines of the file, highlighting the
        before/after state.
        """
        Color.echo('[prompt]This is the whole file with all strings '
                   'migrated.[end]')
        Color.echo('[prompt]{}[end]'.format('-' * 72))

        for string_migration in file_migration.strings:
            if not string_migration.modified:
                Color.echo(string_migration.original)
            else:
                if string_migration.confidence == Confidence.LOW:
                    Color.echo('[warn]--- [Low confidence!][end]')

                Color.echo('[red]{}[end]'.format(
                    add_line_prefix(string_migration.original, '- ')))

                Color.echo('[green]{}[end]'.format(
                    add_line_prefix(string_migration.new, '+ ')))

        Color.echo('[prompt]{}[end]'.format('-' * 72))
Beispiel #23
0
 def save_file(self, file_migration):
     Color.echo('Dry-run: no file was saved')
     return False, None
Beispiel #24
0
    def _show_results(self, files, stats):
        """Show a detailed report of how the migration went.

        :param list files: a list of TranslatableFile objects
        :param dict stats: a dictionary with all statistics of the execution
        """
        Color.echo('\n\n[high]Migration completed![end]')
        Color.echo('--------------------')
        Color.echo('[high]Files found:[end] [warn]{}[end]'.format(len(files)))
        Color.echo('[high]Files processed:[end] [warn]{}[end]'.format(
            stats['processed_files']))

        files_modified = 0
        strings_modified = 0
        for _, file_migration in stats['migrations']:
            new_string_count = len(file_migration.modified_strings)
            strings_modified += new_string_count
            if new_string_count:
                files_modified += 1

        # Files & string migrations
        Color.echo('[high]File migrations created:[end] [warn]{}[end]'.format(
            files_modified))
        Color.echo(
            '[high]String migrations inside these files: [warn]{}[end]'.format(
                strings_modified))

        # Files saved
        Color.echo('[high]Files saved:[end] [warn]{}[end]'.format(
            len(stats['saved'])))
        saved_str = '\n'.join(
            [' - [file]{}[end]'.format(x.filename) for x in stats['saved']])
        if saved_str:
            Color.echo(saved_str)

        # Files & strings marked
        Color.echo(
            '[high]Files marked for proofreading:[end] [warn]{}[end]'.format(
                stats['files_marked']))
        Color.echo(
            '[high]Strings marked for proofreading:[end] [warn]{}[end]'.format(
                stats['strings_marked']))

        # Errors found
        Color.echo('[high]Errors found:[end] [warn]{}[end]'.format(
            len(stats['errors'])))
        errors_str = '\n'.join(
            [' - [warn]{}[end]'.format(x.filename) for x in stats['errors']])
        if errors_str:
            Color.echo(errors_str)

        Color.echo('')
Beispiel #25
0
    def print_diff_only(file_migration):
        """Print only the lines that are different, showing the
        before/after state.
        """
        Color.echo('[prompt]These are the modified strings[end]'
                   ' (the rest of the file is omitted)')
        Color.echo('[prompt]-------------------------------------[end]')

        for string_migration in file_migration.modified_strings:
            if string_migration.confidence == Confidence.LOW:
                Color.echo('[warn]--- [Low confidence!][end]')

            Color.echo('[red]{}[end]'.format(
                add_line_prefix(string_migration.original, '- ')))
            Color.echo('[green]{}[end]\n'.format(
                add_line_prefix(string_migration.new, '+ ')))
        Color.echo('[prompt]-------------------------------------[end]')
Beispiel #26
0
 def verbose(self, msg):
     if self.verbose_output:
         Color.echo(msg)
Beispiel #27
0
 def output(self, msg):
     Color.echo(msg)
Beispiel #28
0
    def transform(self, src, filename=None):
        """Parse the given Python file string and extract translatable content.

        :param unicode src: a chunk of Python code
        :param str filename: the filename of the code, i.e. the filename it
            came from
        :return: a list of SourceString objects
        :rtype: list
        """
        # Replace utf-8 magic comment, to avoid getting a
        # "SyntaxError: encoding declaration in Unicode string"
        src = ENCODING_PATTERN.sub('# ', src)
        try:
            tree = ast.parse(src)
            visitor = CallDetectionVisitor([(x['modules'], x['function'])
                                            for x in self._functions])
            visitor.visit(tree)

        except Exception as e:
            Color.echo('[error]Error while parsing content '
                       'of [file]{}[end]: {}'.format(filename, e))
            # Store an exception for this particular file
            self.errors.append((filename, e))
            return None

        else:
            attree = asttokens.ASTTokens(src, tree=tree)
            file_migration = FileMigration(filename, src)
            last_migrated_char = 0

            def add_in_between(txt_range):
                """Add the simple text found between the last migrated node
                and the current node.

                Ensures that the text that should remain untransformed
                is included in the final file.
                """
                if txt_range[0] > last_migrated_char:
                    try:
                        original_str = src[last_migrated_char:txt_range[0]]
                        file_migration.add_string(
                            StringMigration(
                                original=original_str,
                                new=original_str,
                                confidence=Confidence.HIGH,
                            ))
                    except Exception as e:
                        Color.echo(
                            '[error]Error while adding in-between content '
                            'in [file]{}[end]. last_migrated_char={}.'
                            'Error: {}'.format(filename, last_migrated_char,
                                               e))
                        raise

            # Create a map with the text range for each node to migrate
            # We need this in order to sort the nodes. This way, we can
            # support the migration of imports that appear after function
            # calls (e.g. locally)
            text_ranges = {}
            to_migrate = ([x.node
                           for x in visitor.imports] + visitor.function_calls)
            for node in to_migrate:
                text_ranges[node] = attree.get_text_range(node)

            # Remove duplicates
            to_migrate = sorted(set(to_migrate),
                                key=lambda n: text_ranges[n][0])

            import_added = False

            # Create a migration for adding the import statement
            # of Native. At this moment we don't know if it will need
            # to include t, lazyt or both, but we'll update the instance
            # after all nodes have been processed
            native_import_string_migration = StringMigration('', '')
            native_functions = set()  # will store 't'/'lazyt' if found later

            for node in to_migrate:
                text_range = text_ranges[node]
                add_in_between(text_range)

                try:
                    original = src[text_range[0]:text_range[1]]
                    new = original
                    confidence = Confidence.HIGH

                    # Migrate ImportFrom nodes. Leave Import nodes intact,
                    # as they may have been added in the code for uses other
                    # than gettext calls
                    if isinstance(node, ast.ImportFrom):
                        try:
                            #
                            if not import_added:
                                file_migration.add_string(
                                    native_import_string_migration)
                            new, item_native_functions = self._transform_import(
                                visitor, node)
                            confidence = Confidence.HIGH
                            import_added = True
                            native_functions.update(item_native_functions)

                            # If the whole import statement was about gettext
                            # functions, a new empty line will have been
                            # added to the file. In that case,
                            # this will also remove the empty line completely
                            if new == '' and \
                                    node.first_token.line == original + '\n':
                                text_range = (text_range[0], text_range[1] + 1)
                                original = original + '\n'
                        except Exception as e:
                            raise Exception('Error while migrating import'
                                            '\n{}'.format(str(e)))

                    # Migrate function calls
                    elif isinstance(node, ast.Call):
                        try:
                            new, confidence = self._transform_call(
                                node, visitor, attree)
                        except Exception as e:
                            raise Exception('Error while migrating call'
                                            '\n{}'.format(str(e)))

                        # If this function call was part of a % operation
                        # make sure the right part is also removed
                        modulo_node = visitor.modulos.get(node)
                        if modulo_node:
                            # Override the text range with that of the
                            # modulo operation. This includes the function
                            # call as well
                            text_range = attree.get_text_range(modulo_node)
                            original = src[text_range[0]:text_range[1]]

                    file_migration.add_string(
                        StringMigration(
                            original=original,
                            new=new,
                            confidence=confidence,
                        ))
                    last_migrated_char = text_range[1]

                except Exception as e:
                    Color.echo('[error]Error while transforming content '
                               'of [file]{}[end]: {}'.format(filename, e))
                    Color.echo('Original content:\n{}'.format(original))
                    # Store an exception for this particular file
                    self.errors.append((filename, e))
                    return None

            # Add the rest of the file (from the last migrated node
            # to the end)
            if last_migrated_char < len(src):
                original = src[last_migrated_char:]
                file_migration.add_string(
                    StringMigration(original=original, new=original))

            # Update the Native import statement with the proper functions
            if native_functions:
                native_import_string_migration.update(
                    extra_original='',
                    extra_new=self.import_statement.format(', '.join(
                        sorted(native_functions))))

            return file_migration
Beispiel #29
0
    def _show_push_results(self, status_code, response_content):
        """Display results of pushing the source strings to CDS.

        :param int status_code: the HTTP status code
        :param dict response_content: the content of the response
        """
        try:
            data = response_content.get('data')
            status = data.get('status')
            errors = data.get('errors', [])
            if status == 'completed':
                details = data.get('details')
                created = details.get('created')
                updated = details.get('updated')
                skipped = details.get('skipped')
                deleted = details.get('deleted')
                failed = details.get('failed')
                Color.echo(
                    '[green]\nSuccessfully pushed strings to Transifex.[end]')

                if created > 0:
                    Color.echo('[high]Created strings:[end] '
                               '[warn]{created}[end]'.format(created=created))

                if updated > 0:
                    Color.echo('[high]Updated strings:[end] '
                               '[warn]{updated}[end]'.format(updated=updated))

                if skipped > 0:
                    Color.echo('[high]Skipped strings:[end] '
                               '[warn]{skipped}[end]'.format(skipped=skipped))

                if deleted > 0:
                    Color.echo('[high]Deleted strings:[end] '
                               '[warn]{deleted}[end]'.format(deleted=deleted))

                if failed > 0:
                    Color.echo('[high]Failed strings:[end] '
                               '[warn]{failed}[end]'.format(failed=failed))
            else:
                Color.echo(
                    '[error]\nCould not push strings to Transifex.[end]')

            if len(errors) > 0:
                Color.echo('[high]Errors:[end] {errors}[end]\n'.format(
                    errors='\n'.join(errors)))
        except Exception:
            self.output('(Error while printing formatted report, '
                        'falling back to raw format)\n'
                        'Status: {code}\n'
                        'Content: {content}'.format(
                            code=status_code,
                            content=response_content,
                        ))
Beispiel #30
0
    def prompt_for_file(self, file_migration):
        """Prompt the user to review the file migration and decide what to do.

        :param FileMigration file_migration: the migration object
        :return: an integer directive that determines what to do with the file
        :rtype: int
        """
        while True:
            reply = self._file_prompt_intro()
            if reply == ACCEPT_CHOICE:
                Color.echo('✅️ Changes accepted')
                return REVIEW_ACCEPT

            elif reply == ACCEPT_ALL_CHOICE:
                Color.echo('\n[warn]WARNING![end]')
                reply = yes_no(
                    'If you continue, all changes in the remaining files '
                    'will be accepted. Are you sure you want to continue?',
                    no_message='Aborted.')
                if reply is True:
                    Color.echo('✅ ✅ Changes in all remaining files accepted')
                    return REVIEW_ACCEPT_ALL

            elif reply == REJECT_CHOICE:
                file_migration.revert()
                Color.echo('❌ Changes in file rejected')
                return REVIEW_REJECT

            elif reply == REJECT_ALL_CHOICE:
                Color.echo('\n[warn]WARNING![end]')
                reply = yes_no(
                    'If you continue, all changes in the remaining files '
                    'will be rejected. Are you sure you want to continue?',
                    no_message='Aborted.')
                if reply is True:
                    Color.echo('❌ ❌ Changes in all remaining files rejected')
                    return REVIEW_REJECT_ALL

            elif reply == MARK_CHOICE:
                mark_string(
                    file_migration.strings[0],
                    self._comment_format,
                    MARK_PROOFREAD_FILE,
                )
                Color.echo(
                    '📝 Changes in file accepted & file marked for proofreading'
                )
                return REVIEW_MARK_STRING

            elif reply == PRINT_DIFF_CHOICE:
                # Print only the lines that are different, showing the diff
                FileDiffOutput.print_diff_only(file_migration)

            elif reply == PRINT_FILE_WITH_DIFF_CHOICE:
                # Print all lines, showing the diff
                FileDiffOutput.print_file_with_diff(file_migration)

            elif reply == PRINT_FILE_CHOICE:
                # Print the new version of the file, highlighting changed chars
                # (not the before/after, just the final result)
                FileDiffOutput.print_new_file(file_migration)

            elif reply == PRINT_ORIGINAL_CHOICE:
                # Print the orinal file
                FileDiffOutput.print_original_file(file_migration)

            elif reply == EXIT_CHOICE:
                file_migration.revert()
                Color.echo('❌ Changes in file rejected')
                Color.echo('❕Exiting the migration')
                return REVIEW_EXIT