42 votes

Comment obtenir le lineno de "fin de déclaration" en Python

Je suis en train de travailler sur un script qui manipule un autre script en Python, le script modifié la structure comme:

class SomethingRecord(Record):
    description = 'This records something'
    author = 'john smith'

J'utilise ast pour localiser le description numéro de la ligne, et je l'utilise un peu de code pour modifier le fichier d'origine avec la nouvelle chaîne de description de base sur le numéro de ligne. So far So good.

Maintenant, la seule question est - description occasionnellement, est un multi-ligne de chaîne, par exemple

    description = ('line 1'
                   'line 2'
                   'line 3')

ou

    description = 'line 1' \
        'line 2' \
        'line 3'

et je n'ai que le numéro de ligne de la première ligne, et non pas les lignes suivantes. Donc ma ligne de replacer le ferait

    description = 'new value'
        'line 2' \
        'line 3'

et le code est cassé. J'ai pensé que si je sais que les deux la lineno de début et de fin/nombre de lignes d' description d'attribution j'ai pu réparer mon code pour gérer une telle situation. Comment puis-je obtenir de telles informations avec Python standard library?

7voto

Ira Baxter Points 48153

J'ai regardé les autres réponses, il apparaît que les gens sont en train de faire des backflips, afin de contourner les problèmes de calcul des numéros de ligne, lorsque votre vrai problème est celui de la modification du code. Que suggère la ligne de base des machines n'est pas vous aider à la façon dont vous avez réellement besoin.

Si vous utilisez un programme de transformation system (PTS), vous pouvez éviter beaucoup de ce non-sens.

Une bonne PTS va analyser le code source d'un AST, puis vous permettent d'appliquer au niveau de la source des règles de réécriture pour modifier l'AST, et enfin convertir la modification de l'AST en arrière dans le texte source. Génériquement PTSes accepter les règles de transformation de cette forme:

   if you see *this*, replace it by *that*

[Un analyseur qui construit un AST n'est PAS un PTS. Ils ne permettent pas de règles comme cela, vous pouvez écrire un code ad hoc de pirater à l'arbre, mais ce n'est généralement assez maladroite. Ne font-ils de l'AST pour le texte de la source de régénération.]

(Mes PTS, voir bio, appelé) DMS est un dps qui pourrait répondre à cela. OP sur un exemple spécifique pourrait se faire facilement à l'aide de la règle de réécriture suivante:

 source domain Python; -- tell DMS the syntax of pattern left hand sides
 target domain Python; -- tell DMS the syntax of pattern right hand sides

 rule replace_description(e: expression): statement -> statement =
     " description = \e "
  ->
     " description = ('line 1'
                      'line 2'
                      'line 3')";

Une règle de transformation est donné un nom replace_description pour le distinguer de tous les autres de la règle, nous pouvons définir. La règle de paramètres (e: l'expression) indiquer le motif de permettre une expression arbitraire tel que défini par la langue source. déclaration->déclaration signifie que la règle de cartes d'une instruction dans la langue source, à une instruction dans la langue cible; on pourrait utiliser n'importe quel autre syntaxe de la catégorie de l'Python grammaire fourni à DMS. La " utilisé ici est un metaquote, utilisé pour distinguer la syntaxe de la règle de forme de langage la syntaxe de l'objet langue. Le deuxième -> sépare le modèle de source de ce à partir de la mire qui.

Vous remarquerez qu'il n'est pas nécessaire de mentionner les numéros de ligne. Le PTS convertit la règle de la surface de la syntaxe correspondants ASTs en fait, l'analyse des modèles avec le même analyseur utilisé pour analyser le fichier source. L'ASTs produit pour les modèles sont utilisés pour effectuer la mise en correspondance du modèle/de remplacement. Parce que ce est entraîné à partir de l'ASTs, la mise en page de l'original du code (espacement, mais les sauts de ligne, commentaires) n'affectent pas DMS capacité de match ou de la remplacer. Les commentaires ne sont pas un problème pour la mise en correspondance parce qu'ils sont attachés aux nœuds de l'arborescence plutôt que de les nœuds de l'arborescence; ils sont conservés dans le programme transformé. DMS ne capture de ligne et de colonne précise de l'information pour tous de l'arbre des éléments; tout simplement pas nécessaires pour mettre en œuvre des transformations. La disposition du Code est également conservée dans la sortie par DMS, l'utilisation de la ligne/colonne d'informations.

D'autres PTSes offrent généralement des fonctionnalités similaires.

6voto

Ohad Eytan Points 4832

Pour contourner le problème, vous pouvez changer:

     description = 'line 1' \
              'line 2' \
              'line 3'
 

à:

     description = 'new value'; tmp = 'line 1' \
              'line 2' \
              'line 3'
 

etc.

C'est un changement simple mais un code vraiment moche produit.

3voto

ivanl Points 421

En effet, les informations dont vous avez besoin n'est pas stockée dans l' ast. Je ne connais pas les détails de ce que vous avez besoin, mais il semble que vous pouvez utiliser l' tokenize module de la bibliothèque standard. L'idée est que chaque logique Python instruction est terminée par un NEWLINE token (en outre, il pourrait être un point-virgule, mais ce que je comprends, il n'est pas votre cas). J'ai testé cette approche avec ce fichier:

# first comment
class SomethingRecord:
    description = ('line 1'
                   'line 2'
                   'line 3')

class SomethingRecord2:
    description = ('line 1',
                   'line 2',
                   # comment in the middle

                   'line 3')

class SomethingRecord3:
    description = 'line 1' \
                  'line 2' \
                  'line 3'
    whatever = 'line'

class SomethingRecord3:
    description = 'line 1', \
                  'line 2', \
                  'line 3'
                  # last comment

Et voici ce que je propose:

import tokenize
from io import BytesIO
from collections import defaultdict

with tokenize.open('testmod.py') as f:
    code = f.read()
    enc = f.encoding

rl = BytesIO(code.encode(enc)).readline
tokens = list(tokenize.tokenize(rl))

token_table = defaultdict(list)  # mapping line numbers to token numbers
for i, tok in enumerate(tokens):
    token_table[tok.start[0]].append(i)

def find_end(start):
    i = token_table[start][-1]  # last token number on the start line
    while tokens[i].exact_type != tokenize.NEWLINE:
        i += 1
    return tokens[i].start[0]

print(find_end(3))
print(find_end(8))
print(find_end(15))
print(find_end(21))

Cette affiche:

5
12
17
23

Cela semble être correct, il pourrait accorder cette approche en fonction de ce dont vous avez besoin. tokenize est plus bavarde que ast mais aussi plus souple. Bien sûr, la meilleure approche est d'utiliser à la fois pour les différentes parties de votre tâche.


EDIT: j'ai essayé ce en Python 3.4, mais je pense qu'il devrait également travailler dans d'autres versions.

1voto

whats_done_is Points 212

Ma solution prend un chemin différent: quand j'ai dû changer de code dans un autre fichier, j'ai ouvert le fichier, trouvé la ligne et obtenu toutes les lignes suivantes avec un retrait plus profond que le premier et renvoyé le numéro de ligne de la première ligne qui n'est pas '. t plus profond. Je retourne None, None si je ne trouve pas le texte que je cherchais. C’est bien sûr incomplet, mais je pense que c’est suffisant pour vous aider :)

 def get_all_indented(text_lines, text_in_first_line):
    first_line = None
    indent = None
    for line_num in range(len(text_lines)):
        if indent is not None and first_line is not None:
            if not text_lines[line_num].startswith(indent):
                return first_line, line_num     # First and last lines
        if text_in_first_line in text_lines[line_num]:
            first_line = line_num
            indent = text_lines[line_num][:text_lines[line_num].index(text_in_first_line)] + ' '  # At least 1 more space.
    return None, None
 

1voto

DS. Points 3577

Il existe une nouvelle bibliothèque asttokens qui résout ce problème: https://github.com/gristlabs/asttokens

 import ast, asttokens

code = '''
class SomethingRecord(object):
    desc1 = 'This records something'
    desc2 = ('line 1'
             'line 2'
             'line 3')
    desc3 = 'line 1' \
            'line 2' \
            'line 3'
    author = 'john smith'
'''

atok = asttokens.ASTTokens(code, parse=True)
assign_values = [n.value for n in ast.walk(atok.tree) if isinstance(n, ast.Assign)]

replacements = [atok.get_text_range(n) + ("'new value'",) for n in assign_values]
print(asttokens.util.replace(atok.text, replacements))
 

produit

 class SomethingRecord(object):
    desc1 = 'new value'
    desc2 = ('new value')
    desc3 = 'new value'
    author = 'new value'
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X