Outils pour utilisateurs

Outils du site


issue143:c_c

Addition to last month After last month’s article came out, I got an email from Ian, who suggested his preferred tool for Markdown to HTML conversion, Remarkable (see the Further Reading section for a link). So, for any readers who are looking for something more like that, you now have a starting place! Additionally, I created/updated my bash script for pandoc (md2pdf), which looks like that shown above. If you want to use this script yourself, make sure the path to the tufte.css file is correct for your system (see last month’s article for more details). The script itself is executed like this: md2pdf Notes-To-Convert.md The script automatically creates the PDF filename using the original markdown filename, and adds a title to the PDF to avoid the warning/error appearing. You could also easily expand this script to work for any number of arguments by looping over “$@” and placing the command within the for loop for each item. I typically won’t need this, or, if I do, I’ll likely want to create a single PDF, which would require a different pandoc command anyways, which is why I left the script a little more basic (and easier to understand). And now back to your regularly scheduled programming…

Complément au mois précédent

Après la sortie de l'article du mois dernier, j'ai reçu un mail de Ian, qui suggérait son outil préféré pour la conversion du Markdown en HTML, Remarkable (voir le lien dans la section Pour aller plus loin). Aussi, pour tous les lecteurs qui cherchent quelque chose dans ce genre, vous pouvez maintenant disposer d'une base de départ !

De plus, j'ai créé/mis à jour mon script bash pour pandoc (md2pdf), qui ressemble à ce qui est montré ci-dessus.

Si vous voulez utiliser vous-même ce script, assurez-vous que le chemin vers le fichier tufte-css est correct pour votre système (voyez l'article du mois dernier pour plus de détails). Le script lui-même s'exécute ainsi :

md2pdf Notes-To-Convert.md

Le script crée automatiquement le nom du fichier PDF en utilisant celui du fichier markdown d'origine et ajoute un titre au pdf pour éviter qu'un avertissement/une erreur se produise. Vous pouvez aussi étendre facilement ce script pour qu'il fonctionne avec une quantité d'arguments en faisant une boucle au-dessus de « $@ » et en plaçant la commande dans la boucle for de chaque élément. Je ne devrais pas en avoir vraiment besoin, ou, si c'est le cas, je voudrais aussi créer un PDF unique, ce qui nécessiterait de toute manière une commande pandoc différente ; c'est pourquoi j'ai conservé le script un peu plus simple (et plus facile à comprendre).

Et maintenant, retournons à notre programmation prévue normalement…

This month, one of the items on my to-do list was to organize my SGF (go game records) files into a format where I can, at a glance, see whether I won or lost, and when I played it. Originally, I had hoped that each file would have the date stored in the SGF information, but it turns out every server has one (or two) variations on data stored in the files. Some had dates, some had copyright information over multiple lines, some had all the metadata on one line, others spread over multiple lines. As such, I decided to make a python script to print out human readable information, without relying on any SGF plugins (as I don’t want the actual moves, just the metadata). Do note, I am condensing the entire process for the sake of this article. My goal is to instill the TDD mindset on my readers, while offering some examples. The full code will be linked at the end of the article, for anyone who wants to pick it apart.

Ce mois-ci, un des sujets de ma liste des choses à faire était de réorganiser mes fichiers SGF (enregistrements de jeux de go) dans un format dans lequel, d'un coup d'œil, je puisse voir si j'ai gagné ou perdu et quand je l'ai joué. Au début, j'avais espéré que chaque fichier aurait eu la date enregistrée dans les informations SGF, mais il s'avère que chaque serveur a une (ou deux) différences dans les données enregistrées dans les fichiers. Certaines ont des dates, d'autres ont des informations de copyright sur plusieurs lignes, certaines ont toutes les métadonnées sur une seule ligne, d'autres étalées sur plusieurs lignes. De ce fait, j'ai décidé de faire un script en python pour imprimer une information lisible par un humain, sans m'appuyer sur des extensions SGF (car je ne veux pas ce que j'ai effectivement joué, seulement les métadonnées).

Notez bien que j'ai résumé l'ensemble du processus pour le bien de cet article. Mon but est d'instiller le mode de raisonnement à mes lecteurs, tout en leur offrant quelques exemples. À la fin de cet article, un lien vous dirige vers le code complet, pour ceux qui veulent en discuter.

First Step The first step was to decide which format to start with - I settled on the Fox Go Server format, as the information was on one line, and should therefore be the least amount of processing to get the information into Python.

Première étape

La première étape a été de décider avec quel format commencer - je me suis fixé sur le format Fox Go Server, car les informations étaient placées sur une ligne, et, de ce fait, devaient représenter le moindre travail pour transférer l'information dans Python.

Second Step Once I had decided what to tackle first, I then set up my folder structure like this: sgf.py init.py _tests.py main.py The main.py file I originally added after finishing the SGF class and the tests, but it won’t hurt anything to have the file ready from the beginning. Also, init.py is empty, but seems to be required for relative imports to work.

Seconde étape

Une fois que j'eus décidé ce que j'attaquais en premier, j'ai ensuite paramétré ma structure de dossier comme ceci :

sgf.py init.py _tests.py main.py

J'avais ajouté le fichier main.py à l'origine après la fin de la classe SGF et des tests, mais ça ne coûte rien d'avoir le fichier prêt dès le début. init.py est vide aussi, mais semble être nécessaire pour que les importations associées puissent marcher.

Third Step - Tests Now for the first file - tests. Following the practices of TDD (and Adam Wathan’s method), I started with my tests instead of any actual code. The starting _tests.py file looked like this: import unittest from sgf import SGF class SGFItemTests(unittest.TestCase): sgfPath = “./fgs-test.sgf” def test_load_singleLine_sgf(self): testItem = SGF(self.sgfPath) self.assertEqual(testItem.getTitle(), “2019-03-03 - Black (16 kyu) VS White (16 kyu) - B+20.50”) if name == 'main': unittest.main() I left it at that, knowing the test would fail. I was also getting warnings and errors from Visual Studio Code about the class not existing before running anything. As such, I skipped running the test and instead worked using the warnings from Code. If, however, this is your first TDD project, I recommend getting in the habit of running the tests at every stage and dealing with the errors as they appear.

Troisième étape - Les tests

Maintenant, pour le premier fichier - tests. En suivant les pratiques de TDD (et la méthode d'Adam Wathan), j'ai commencé avec mes tests plutôt qu'avec du vrai code.

Le fichier _tests.py du début ressemblait à ceci :

import unittest

from sgf import SGF

class SGFItemTests(unittest.TestCase):

  sgfPath = "./fgs-test.sgf"
  def test_load_singleLine_sgf(self):
      testItem = SGF(self.sgfPath)
      self.assertEqual(testItem.getTitle(), "2019-03-03 - Black (16 kyu) VS White (16 kyu) - B+20.50")

if name == 'main':

  unittest.main()
  
  Je l'ai laissé ainsi, sachant que le test échouerait. J'ai eu aussi des avertissements et des erreurs de Visual Studio Code à propos de la classe qui n'existait pas avant d'avoir lancé quoi que ce soit. De ce fait, j'ai sauté les tests et, à la place, j'ai travaillé avec les avertissements de Code. Cependant, si c'est votre premier projet TDD, je vous recommande de prendre l'habitude de lancer les tests à chaque étape et de prendre en compte les erreurs quand elles apparaissent.

Fourth Step - Actual Development sgf.py import re #this is required for the regex code in the future class SGF: def init(self, path): self.title = “created” All I did here was make sure I could import the python file and that it had a constructor. I then began running the tests, and fixing each error as it occurred. First it required me to create a getTitle() function, then I expanded the constructor to loop through the file path and pass each line through to a createTitle function that checked for the existence of specific data (such as PB[], PW[], date[], WR[], BR[], and RE[]). Those fields are player (black), player (white), the date, the players’ ranks, and the result.

Quatrième étape - Le vrai développement

sgf.py import re #ceci est nécessaire dans le futur pour le code des regex (expression régulière)

class SGF:

  def __init__(self, path):
      self.title = “created”
      

Tout ce que j'ai fait ici, c'est de m'assurer que je pouvais importer le fichier python et qu'il avait un constructeur ; ensuite, j'ai commencé à lancer les tests et à résoudre chaque erreur quand elle apparaisait. D'abord, j'ai dû créer une fonction getTitle(), puis j'ai étendu le constructeur pour boucler dans le chemin du fichier et passer chaque ligne dans une fonction createTitle qui vérifiait l'existence de données particulières (telles que PB[], PW[], date[], WR[], BR[] et RE[]). Ces champs sont le joueur (des noirs), le joueur (des noirs), le joueur (des blancs), la date, le niveau de chaque joueur et le résultat.

Admittedly, I stretched those steps out slowly - first I tried to grab the player names and had my test written for that, and so on, evolving both the class and my tests. For the sake of this article, I’m condensing some steps. The regex I used was as follows: name = re.search('PW\[(.+?)\]', string) if name: white['name'] = name.group(1)

Certes, j'ai déterminé ces étapes lentement : d'abord, j'ai essayé de récupérer le nom des joueurs et j'ai écrit mon test pour cela, et ainsi de suite, faisant évoluer à la fois ma classe et mes tests. Par souci pour l'article, j'ai condensé certaines étapes.

La regex que j'ai utilisée était ainsi :

name = re.search('PW\[(.+?)\]', string)

if name:

 white['name'] = name.group(1)

The important part of this code are the normal brackets “()”, which creates a group of all the characters between the square brackets (which are the values I’m after). The name.group(1) line simply loads the saved group into a string. I changed the value I was looking for, but the basic framework remained the same. As you can see, I started saving dictionaries for the various values to make the code more readable. Essentially the entire class became a series of functions to strip out corresponding information (player information, results, date), and the information was then fed back into a class-wide dictionary called “info”. The getTitle function eventually became a function that simply reads the information out of info, and parses it into a nice string. I also expanded my tests to check various sub items (instead of just the title) by creating a function called getPlayers, and then checking the various fields.

La partie importante du code sont les parenthèses « () » qui créent un groupe de tous les caractères entre crochets (qui sont les valeurs que je cherche). Le ligne de name.group(1) charge simplement le groupe sauvegardé dans une chaîne de caractères.

J'ai changé la valeur que je cherchais, mais le cadre de base est resté le même. Comme vous pouvez le voir, j'ai commencé à sauvegarder les dictionnaires des différentes valeurs pour rendre le code plus lisible. En gros, la classe entière devint une série de fonctions pour extraire l'information correspondante (information sur le joueur, les résultats, la date) et l'information fut ensuite introduite dans un dictionnaire de classe appelé « info ». La fonction getTitle devint finalement une fonction qui ne fait que lire les éléments tirés des informations et les assemble dans une jolie chaîne. J'ai aussi étendu mes tests pour vérifier plusieurs sous-éléments (plutôt que le titre seul) en créant une fonction appelée getPlayers, puis en vérifiant les différents champs.

Fifth Step - Next Test The entire above step was dedicated to having my test “test_load_singleLine_sgf” pass successfully. The reason I did it this way was as a proof of concept, and to refine the various functions for parsing the data. This means that all I had left to do was upgrade my file parsing function to not fail when all the metadata isn’t on one line. It doesn’t matter if there are extra items, as the regex will pick out only what I’m looking for. I then created a new test called “test_load_multiLine_sgf”, and let it load a game from OGS, which was split up over multiple lines. The first goal was to again load the player data properly (both black and white), which required me to devise a check for whether or not the metadata was over multiple lines. I opened up an online regex tester, put in some test data, and experimented a bit until I found a regex that seemed to work.

Cinquième étape - Test suivant

L'étape ci-dessus a été entièrement consacrée à la réussite de mon test « test_load_singleLine_sgf ». Je l'ai fait ainsi car c'était comme une preuve de la conception et pour affiner les différentes fonctions pour l'analyse des données. Cela signifie que la seule chose qui me restait à faire était la mise à niveau de la fonction d'analyse de mon fichier pour qu'elle ne plante pas quand toutes les métadonnées ne sont pas sur une seule ligne. L'existence d'éléments supplémentaires n'a pas d'importance, car la regex ne récupérera que ce que je cherche. Ensuite, j'ai créé un nouveau test « test_load_multiLine_sgf » et l'ai fait charger un jeu de OGS, qui était découpé sur plusieurs lignes.

Le premier but était à nouveau de charger les données des joueurs proprement (les noirs comme les blancs), ce qui a nécessité que j'élabore un contrôle si les données étaient sur plusieurs lignes ou non. J'ai ouvert un testeur de regex en ligne, mis quelques données de test et testé un moment jusqu'à ce que je trouve une regex qui semblait fonctionner.

The entire checkMultiline function ended up looking like this: def checkMultiline(self, string): multiline = re.search('[a-zA-Z]+\[.+?\]\n', string) if multiline: return True else: return False What the regex does is to search for any characters (upper or lowercase) that precede a square bracket, some characters, a closing square bracket, and a newline. I wasn’t too worried about only matching exactly the metadata lines, as I never read the entire file (I break out of the loop once I find all the information I need), and the secondary regex will not be affected. The check is used in my readSGF function, and every line that matches the multiline check is then strung together into a single string (without newlines), which is passed through to the various functions.

La fonction complète checkMultiline a fini par ressembler à ceci :

def checkMultiline(self, string):

  multiline = re.search('[a-zA-Z]+\[.+?\]\n', string)
  if multiline:
     return True
  else:
     return False

Ce que fait la regex est de chercher tous les caractères (en majuscules ou en minuscules) qui précèdent le crochet, certains caractères, un crochet fermant et une nouvelle ligne. Je n'étais pas trop inquiet de ne correspondre exactement qu'aux lignes de métadonnées, car je ne lisais jamais tout le fichier (je sors de la boucle une fois que je trouve toute l'information dont j'ai besoin) et la regex secondaire ne sera pas affectée. La vérification est utilisée dans ma fonction readSGF et toutes les lignes qui correspondent à la vérification des lignes multiples sont chaînées en une ligne unique (sans retour à la ligne) qui est envoyée aux diverses fonctions.

This worked fine for OGS (except reviews) files, and then I tested it on Pandanet (IGS) files, where it promptly broke. The reason it broke was simple - Pandanet added a Copyright value into the metadata, and spread it over 4 or 5 lines (depending on where the SGF was created). I put Pandanet in a separate test, and focused only on that test. Running a single test in Python is as simple as: python _test.py SGFItemTests.test_load_pandanet_sgf. I quickly concluded that using regex for this particular case was going to be tricky, as the number of lines wasn’t always uniform. Instead, I decided to adapt my readSGF function to simply not process the following lines when it discovers the Copyright value.

Cela fonctionnait bien pour les fichiers OGS (sauf les revues) ; je l'ai testé ensuite sur les fichiers Pandanet (IGS), où elle a rapidement échoué. La raison de l'échec est simple - Pandanet a ajouté une valeur de Copyright dans les métadonnées, et l'a répartie sur 4 ou 5 lignes (selon l'endroit où le SGF a été créé). J'ai mis Pandanet dans un test séparé et je me suis concentré uniquement sur ce test. Lancer un test unique dans Python est aussi simple que :

python _test.py SGFItemTests.test_load_pandanet_sgf

J'en ai rapidement conclu que l'utilisation d'une regex dans ce cas particulier allait être délicate, car le nombre de lignes n'était pas toujours uniforme. À la place, j'ai décidé d'adapter ma fonction readSGF pour qu'elle ne traite tout simplement pas les lignes qui suivent quand elle découvre la valeur du Copyright.

I do this by initializing a tempCount at 0, and setting it to a value of 6 when I can find “CoPyright[\n” in the string. I also added an ‘if’ to see if tempCount is greater than 0, and when it is, the counter is reduced by one and the loop follows the “continue” directive (where it jumps to the next item in the loop). This effectively skips the plain english lines of text, removing the problems. I also noticed that some SGF files had a CP[] copyright line (such as the OGS review files), which was shorter than CoPyright. As such, I simply initialized tempCount at 5, instead of 6, which worked fine. The only reason I could do this was that the copyright notices always appeared before the game information, which means I didn’t need to take that into consideration. I realize that this last section can be confusing to read. However, this is pretty much the final file, so viewing the links below should help clarify things. There were a few steps afterwards (such as when a file had no date), but they were simple enough to catch and solve when listening to the tests and batch running the file.

Je fais ceci en initialisant un compteur temporaire tempCount à 0, puis en le réglant à une valeur de 6 quand je veux trouver « CoPyright[\n » dans la chaîne. J'ai ajouté aussi un « if » pour voir si tempCount est plus grand que 0 et, quand il l'est, je le réduis de 1 et la boucle suit la directive « continue » (quand elle saute à l'élément suivant de la boucle). Cela saute effectivement les lignes de texte anglais brut, supprimant les problèmes. J'ai aussi noté que certains fichiers SGF avaient une ligne de copyright CP[] (comme dans les fichiers de revue OGS), qui était plus courte que CoPyright. De ce fait, j'ai simplement initialisé tempCount à 5, au lieu de 6, ce qui marchait bien. Le seule raison pour laquelle je pouvais le faire était que les notes de copyright apparaissaient toujours avant les informations sur le jeu, ce qui signifie que je n'avais pas besoin de les prendre en considération.

Je réalise que cette dernière section peut être confuse à la lecture. Cependant, c'est à peu près le fichier final ; donc, la lecture des contenus des liens ci-dessous devrait clarifier les choses. Il y avait quelques étapes à la suite (comme quand un fichier n'a pas de date), mais elles étaient suffisament simples pour les voir et les résoudre quand je vérifiais les tests et lançais le fichier par lot.

Conclusion Anyone who follows the link to the Gist will notice a few things. Firstly, I sanitized the test files to remove any identifiable information. Especially since readers won’t have my test files and will therefore need to adjust the tests, I felt it helpful to label the information more generically. Secondly, there’s a bash file included. The reason for this is simple - I didn’t want to install the python script into a folder in my $PATH, as it would include other files as well and break the tests. Instead, I wrote the bash script in my $PATH, which appends the full path to the files, and then runs the Python script within its actual folder with absolute paths. You’ll need to adjust the path to main.py for your own system. I hope this look into my TDD process might help inspire some readers to give it a shot, just as I have been inspired by others. Also, if there are any fellow Go players out there - perhaps you’ll find this tool useful for organizing your own SGF files. If you have any questions, suggestions, or comments, they can be directed to me at lswest34+fcm@gmail.com.

Conclusion

Tous ceux qui suivront le lien vers Gist remarqueront certains points. D'abord, j'ai nettoyé les fichiers de test pour enlever toute information d'identification. Particulièrement parce que les lecteurs n'auront pas mes fichiers de test et auront ainsi besoin d'adapter les tests, j'ai trouvé utile d'étiqueter les informations avec des noms génériques. Deuxièmement, un fichier bash est inclus. Cela a une raison simple - je ne voulais pas installer le script en python dans un dossier de mon $PATH, car il pourrait contenir aussi d'autres fichiers et casser les tests. À la place, j'ai écrit le script bash dans mon $PATH, qui ajoute le chemin complet aux fichiers et, ensuite, lance le script en python dans son vrai dossier avec les chemins absolus. Vous aurez à adapter le chemin vers main.py sur votre propre système.

J'espère que cet aperçu de mon processus TDD pourra donner l'idée à quelques lecteurs de l'essayer, tout comme j'ai été inspiré par d'autres. Aussi, s'il se trouve des joueurs de Go aux alentours, vous trouverez peut-être cet outil utile pour organiser vos propres fichiers SGF. Si vous avez des questions, suggestions ou commentaires, vous pouvez me les adresser à lswest34+fcm@gmail.com.

Homework (Optional) My own goal for this script it to expand it over time. My first revision would be to add a stats calculation system, which will give me the overall stats across all the servers I play on (perhaps even details on wins against stronger/weaker opponents). If any reader is so inclined, feel free to take this suggestion and use it as practice yourself! Further Reading http://remarkableapp.github.io/ - Remarkable App’s website https://gist.github.com/lswest/1e7fe8751e0d77f880db7d0a266e652f - A Gist containing all my code for this article.

Travail personnel (Optionnel)

Mon but personnel pour ce script est de l'étendre avec le temps. Ma première révision serait l'ajout d'un système de calcul de statistiques, qui me donnera l'ensemble des statistiques sur tous les serveurs sur lesquels je joue (peut-être même avec des détails sur mes victoires sur des adversaires plus ou moins forts que moi). Si des lecteurs se sentent aussi intéressés, qu'ils n'hésitent pas à s'emparer de cette suggestion et à l'utiliser comme exercice eux-mêmes !

Pour aller plus loin

http://remarkableapp.github.io/ - Site Web de l'appli Remarkable

https://gist.github.com/lswest/1e7fe8751e0d77f880db7d0a266e652f - Un Gist contenant tout mon code pour cet article.

issue143/c_c.txt · Dernière modification : 2019/04/10 10:24 de auntiee