Pourquoi vous devriez utiliser pathlib

Du traducteur: Bonjour, Habr! Je vous présente la traduction de l'article Pourquoi vous devriez utiliser pathlib et sa suite, Non vraiment, pathlib est génial . Une grande attention est désormais accordée aux nouvelles fonctionnalités Python telles que asyncio, l'opérateur: = et la saisie facultative. Dans le même temps, un risque non significatif (bien que: appeler une innovation sérieuse une langue ne se révèle pas une innovation sérieuse) pour un radar, mais des innovations très utiles dans une langue. En particulier, sur une multitude d'articles consacrés à un sujet, je n'ai pas trouvé (sauf un paragraphe ici ), j'ai donc décidé de corriger la situation.


Quand j'ai découvert le nouveau module pathlib il y a quelques années, j'ai décidé du fond de mon esprit que c'était juste une version orientée objet légèrement maladroite du module os.path . J'avais tort. pathlib est vraiment merveilleux !


Dans cet article, je vais essayer de tomber amoureux de pathlib . J'espère que cet article vous pathlib à utiliser pathlib dans toute situation concernant l'utilisation de fichiers en Python .



Partie 1


os.path maladroit


Le module os.path toujours été ce que nous os.path pour les chemins Python. En principe, il y a tout ce dont vous avez besoin, mais souvent il n'a pas l'air trop élégant.


Dois-je l'importer comme ça?


 import os.path BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates') 

Ou alors?


 from os.path import abspath, dirname, join BASE_DIR = dirname(dirname(abspath(__file__))) TEMPLATES_DIR = join(BASE_DIR, 'templates') 

Peut-être que la fonction de join a un nom trop général, et nous devrions faire quelque chose comme ceci:


 from os.path import abspath, dirname, join as joinpath BASE_DIR = dirname(dirname(abspath(__file__))) TEMPLATES_DIR = joinpath(BASE_DIR, 'templates') 

Pour moi, toutes les options ci-dessus ne semblent pas trop pratiques. Nous transmettons des chaînes aux fonctions qui renvoient des chaînes que nous transmettons aux fonctions suivantes qui fonctionnent avec des chaînes. Il se trouve qu'ils contiennent tous des chemins, mais ce ne sont encore que des lignes.


L'utilisation de chaînes pour l'entrée et la sortie dans les fonctions os.path très gênante car vous devez lire le code de l'intérieur. Je voudrais convertir ces appels de imbriqués en séquentiels. C'est ce que pathlib vous permet de faire!


 from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent TEMPLATES_DIR = BASE_DIR.joinpath('templates') 

Le module os.path nécessite des appels de fonction imbriqués, mais pathlib nous permet de créer des chaînes d'appels consécutifs aux méthodes et attributs de la classe Path avec un résultat équivalent.


Je sais ce que vous en pensez: arrêtez, ces objets Path ne sont plus les mêmes qu'avant, nous n'opérons plus sur les lignes de trajectoire! Nous reviendrons sur cette question plus tard (indice: dans presque toutes les situations, ces deux approches sont interchangeables).


os surchargé


Le module os.path classique os.path conçu pour fonctionner avec des chemins. Mais après avoir voulu faire quelque chose avec le chemin (par exemple, créer un répertoire), vous devrez accéder à un autre module, souvent os .


os contient un tas d'utilitaires pour travailler avec des fichiers et des répertoires: mkdir , getcwd , chmod , stat , remove , rename , rmdir . Aussi chdir , link , walk , listdir , makedirs , removedirs , removedirs , unlink , symlink . Et tout un tas de choses qui ne sont pas du tout liées aux systèmes de fichiers: fork , getenv , putenv , environ , getlogin , system , ... Quelques dizaines d'autres choses que je ne mentionnerai pas ici.


Le module os est conçu pour un large éventail de tâches; c'est une telle boîte avec tout ce qui concerne le système d'exploitation. Il y a beaucoup d'utilités dans os , mais ce n'est pas toujours facile de s'y retrouver: il est souvent nécessaire de fouiller un peu dans le module avant de trouver ce dont on a besoin.


pathlib transfère la plupart des fonctions du système de fichiers vers les objets Path .


Voici le code qui crée le src/__pypackages__ et renomme notre fichier .editorconfig en src/.editorconfig :


 import os import os.path os.makedirs(os.path.join('src', '__pypackages__'), exist_ok=True) os.rename('.editorconfig', os.path.join('src', '.editorconfig')) 

Voici un code similaire utilisant Path


 from pathlib import Path Path('src/__pypackages__').mkdir(parents=True, exist_ok=True) Path('.editorconfig').rename('src/.editorconfig') 

Notez que le deuxième exemple de code est beaucoup plus facile à lire, car il est organisé de gauche à droite - tout cela grâce aux chaînes de méthodes.


N'oubliez pas glob


Non seulement os et os.path contiennent des méthodes liées au système de fichiers. Il convient également de mentionner le glob , qui ne peut pas être qualifié d'inutile.


Nous pouvons utiliser la fonction glob.glob pour rechercher des fichiers selon un modèle spécifique:


 from glob import glob top_level_csv_files = glob('*.csv') all_csv_files = glob('**/*.csv', recursive=True) 

Le module pathlib fournit également des méthodes similaires:


 from pathlib import Path top_level_csv_files = Path.cwd().glob('*.csv') all_csv_files = Path.cwd().rglob('*.csv') 

Après le passage au module pathlib , le besoin du glob disparaît complètement : tout ce dont vous avez besoin fait déjà partie intégrante des objets Path


pathlib rend les choses simples encore plus faciles


pathlib simplifie de nombreuses situations difficiles, mais rend également certains extraits de code simples encore plus faciles .


Vous voulez lire tout le texte dans un ou plusieurs fichiers?


Vous pouvez ouvrir le fichier, lire le contenu et fermer le fichier à l'aide du bloc with :


 from glob import glob file_contents = [] for filename in glob('**/*.py', recursive=True): with open(filename) as python_file: file_contents.append(python_file.read()) 

Ou vous pouvez utiliser la méthode read_text sur les objets Path et générer des listes pour obtenir le même résultat en une seule expression:


 from pathlib import Path file_contents = [ path.read_text() for path in Path.cwd().rglob('*.py') ] 

Mais que faire si vous devez écrire dans un fichier?


Voici à quoi cela ressemble en utilisant open :


 with open('.editorconfig') as config: config.write('# config goes here') 

Ou vous pouvez utiliser la méthode write_text :


 Path('.editorconfig').write_text('# config goes here') 

Si, pour une raison quelconque, vous devez utiliser open , soit comme gestionnaire de contexte, soit pour vos préférences personnelles, Path propose la méthode open comme alternative:


 from pathlib import Path path = Path('.editorconfig') with path.open(mode='wt') as config: config.write('# config goes here') 

Ou, à partir de Python 3.6, vous pouvez passer votre Path directement pour open :


 from pathlib import Path path = Path('.editorconfig') with open(path, mode='wt') as config: config.write('# config goes here') 

Les objets de chemin rendent votre code plus évident


Qu'indiquent les variables suivantes? Quelle est la signification de leur signification?


 person = '{"name": "Trey Hunner", "location": "San Diego"}' pycon_2019 = "2019-05-01" home_directory = '/home/trey' 

Chaque variable pointe vers une ligne. Mais chacun d'eux a des significations différentes: le premier est JSON, le second est la date et le troisième est le chemin du fichier.


Cette représentation d'objets est légèrement plus utile:


 from datetime import date from pathlib import Path person = {"name": "Trey Hunner", "location": "San Diego"} pycon_2019 = date(2019, 5, 1) home_directory = Path('/home/trey') 

Les objets JSON peuvent être désérialisés dans un dictionnaire, les dates peuvent être représentées en mode natif à l'aide de datetime.date et les objets de chemin de fichier peuvent être représentés comme Path


L'utilisation d'objets Path rend votre code plus explicite. Si vous souhaitez travailler avec des dates, vous utilisez la date . Si vous souhaitez travailler avec des chemins de fichier, utilisez Path .


Je ne suis pas un très grand partisan de la POO. Les classes ajoutent une couche supplémentaire d'abstraction, et les abstractions ont parfois tendance à compliquer le système plutôt qu'à le simplifier. En même temps, je pense que pathlib.Path est une abstraction utile . Assez rapidement, cela devient une décision acceptée.


Grâce au PEP 519 , les Path deviennent la norme pour travailler avec les chemins. Au moment de Python 3.6, la plupart des os.path os , shutil , os.path fonctionnent correctement avec ces objets. Vous pouvez passer à pathlib , transparent pour votre base de code!


Qu'est-ce qui manque dans pathlib ?


Bien que pathlib cool, il n'est pas complet. Il y a certainement plusieurs possibilités que j'aimerais inclure dans le module .


La première chose qui me vient à l'esprit est le manque de méthodes de chemin d'accès équivalentes à shutil . Bien que vous puissiez passer Path tant shutil paramètres de shutil pour copier / supprimer / déplacer des fichiers et des répertoires, vous ne pouvez pas les appeler en tant que méthodes sur les objets Path .


Donc, pour copier des fichiers, vous devez faire quelque chose comme ceci:


 from pathlib import Path from shutil import copyfile source = Path('old_file.txt') destination = Path('new_file.txt') copyfile(source, destination) 

Il n'y a pas non plus d'analogue de la méthode os.chdir . Cela signifie que vous devez l'importer si vous devez modifier le répertoire actuel:


 from pathlib import Path from os import chdir parent = Path('..') chdir(parent) 

Il n'y a pas non plus d'équivalent à la fonction os.walk . Bien que vous puissiez écrire votre propre fonction dans l'esprit d'une walk sans trop de difficulté.


J'espère qu'un jour les objets pathlib.Path contiendront des méthodes pour certaines des opérations mentionnées. Mais même dans ce scénario, je trouve qu'il est beaucoup plus facile d'utiliser pathlib avec autre chose que d'utiliser os.path et tout le reste .


Est-il toujours nécessaire d'utiliser pathlib ?


À partir de Python 3.6, les chemins fonctionnent presque partout où vous utilisez des chaînes . Je ne vois donc aucune raison de ne pas utiliser pathlib si vous utilisez Python 3.6 et supérieur.


Si vous utilisez une version antérieure de Python 3, vous pouvez à tout moment encapsuler l'objet Path dans un appel str pour obtenir une chaîne si vous devez revenir au pays des lignes. Ce n'est pas trop élégant, mais ça marche:


 from os import chdir from pathlib import Path chdir(Path('/home/trey')) #   Python 3.6+ chdir(str(Path('/home/trey'))) #      

Partie 2. Réponses aux questions.


Après la publication de la première partie, certaines personnes ont posé des questions. Quelqu'un a dit que j'avais malhonnêtement pathlib os.path et pathlib . Certains ont dit que l'utilisation d' os.path tellement ancrée dans la communauté Python que le passage à une nouvelle bibliothèque prendra très longtemps. J'ai également vu quelques questions sur les performances.


Dans cette partie, je voudrais commenter ces questions. Cela peut être considéré à la fois comme pathlib protection pathlib et une sorte de lettre d'amour au PEP 519 .


os.path être honnête, comparez os.path et pathlib


Dans la dernière partie, j'ai comparé les deux fragments de code suivants:


 import os import os.path os.makedirs(os.path.join('src', '__pypackages__'), exist_ok=True) os.rename('.editorconfig', os.path.join('src', '.editorconfig')) 

 from pathlib import Path Path('src/__pypackages__').mkdir(parents=True, exist_ok=True) Path('.editorconfig').rename('src/.editorconfig') 

Cela peut sembler une comparaison injuste, car l'utilisation de os.path.join dans le premier exemple garantit que les délimiteurs corrects sont utilisés sur toutes les plates-formes, ce que je n'ai pas fait dans le deuxième exemple. En fait, tout est en ordre, car Path normalise automatiquement les séparateurs de chemin


Nous pouvons le prouver en regardant la conversion de l'objet Path en une chaîne sous Windows:


 >>> str(Path('src/__pypackages__')) 'src\\__pypackages__' 

Cela ne fait aucune différence si nous utilisons la méthode joinpath , le '/' dans la ligne de chemin, l'opérateur / (une autre fonctionnalité intéressante de Path ), ou si nous transmettons des arguments individuels au constructeur Path, nous obtenons le même résultat:


 >>> Path('src', '.editorconfig') WindowsPath('src/.editorconfig') >>> Path('src') / '.editorconfig' WindowsPath('src/.editorconfig') >>> Path('src').joinpath('.editorconfig') WindowsPath('src/.editorconfig') >>> Path('src/.editorconfig') WindowsPath('src/.editorconfig') 

Le dernier exemple a provoqué une certaine confusion chez les personnes qui ont suggéré que pathlib pas assez intelligent pour remplacer / par \ dans la chaîne de chemin. Heureusement, tout est en ordre!


Avec les objets Path , vous n'avez plus à vous soucier de la direction des barres obliques: définissez tous vos chemins à l'aide de / , et le résultat sera prévisible pour n'importe quelle plate-forme.


Vous n'avez pas à vous soucier de normaliser les chemins.


Si vous utilisez Linux ou Mac, il est très facile d'ajouter accidentellement des bogues au code qui affectent uniquement les utilisateurs de Windows. Si vous ne surveillez pas attentivement l'utilisation de os.path.join et \ ou os.path.normcase pour convertir les barres obliques en celles adaptées à la plate-forme actuelle, vous pouvez écrire du code qui ne fonctionnera pas correctement sous Windows .


Voici un exemple de bogue spécifique à Windows:


 import sys import os.path directory = '.' if not sys.argv[1:] else sys.argv[1] new_file = os.path.join(directory, 'new_package/__init__.py') 

De plus, un tel code fonctionnera correctement partout:


 import sys from pathlib import Path directory = '.' if not sys.argv[1:] else sys.argv[1] new_file = Path(directory, 'new_package/__init__.py') 

Auparavant, le programmeur était responsable de la concaténation et de la normalisation des chemins, tout comme dans Python 2, le programmeur était responsable de décider où utiliser unicode au lieu d'octets. Ce n'est plus votre tâche - Path résout tous ces problèmes pour vous.


Je n'utilise pas Windows et je n'ai pas d'ordinateur Windows. Mais beaucoup de gens qui utiliseront mon code utiliseront très probablement Windows, et je veux que tout fonctionne correctement pour eux.


S'il y a une chance que votre code s'exécute sur Windows, vous devriez sérieusement envisager de passer à pathlib .


Ne vous inquiétez pas de la normalisation : utilisez quand même Path pour les chemins de fichiers.


Ça a l'air cool, mais j'ai une bibliothèque tierce qui n'utilise pas pathlib !


Vous disposez d'une grande base de code qui fonctionne avec des chaînes comme chemins. Pourquoi passer à pathlib si cela signifie que tout doit être réécrit?


Imaginons que vous ayez la fonction suivante:


 import os import os.path def make_editorconfig(dir_path): """Create .editorconfig file in given directory and return filename.""" filename = os.path.join(dir_path, '.editorconfig') if not os.path.exists(filename): os.makedirs(dir_path, exist_ok=True) open(filename, mode='wt').write('') return filename 

La fonction prend un répertoire et y crée un fichier .editorconfig , quelque chose comme ceci:


 >>> import os.path >>> make_editorconfig(os.path.join('src', 'my_package')) 'src/my_package/.editorconfig' 

Si vous remplacez les lignes par Path , tout fonctionnera aussi:


 >>> from pathlib import Path >>> make_editorconfig(Path('src/my_package')) 'src/my_package/.editorconfig' 

Mais ... comment?


os.path.join accepte les objets Path (depuis Python 3.6). On peut en dire os.makedirs des os.makedirs .
En fait, la fonction open intégrée accepte Path , shutil accepte Path et tout dans la bibliothèque standard utilisée pour accepter les chaînes devrait maintenant fonctionner avec Path et les chaînes.


Nous devons remercier PEP 519 pour cela , qui a fourni la classe abstraite os.PathLike et a annoncé que tous les utilitaires intégrés pour travailler avec les chemins de fichiers devraient maintenant fonctionner avec les chaînes et Path .


Mais ma bibliothèque préférée a Path, meilleure que la norme!


Vous utilisez peut-être déjà une bibliothèque tierce qui fournit son implémentation Path , qui est différente de celle standard. Peut-être que vous l'aimez plus.


Par exemple, django-environ , path.py , plumbum et visidata contiennent chacun leurs propres objets Path . Certaines de ces bibliothèques sont plus anciennes que pathlib et ont décidé d'hériter de str afin qu'elles puissent être passées à des fonctions qui attendent des chaînes comme chemins. Grâce au PEP 519, l'intégration de bibliothèques tierces dans votre code sera plus facile et sans avoir besoin d'hériter de str .


Imaginons que vous ne vouliez pas utiliser pathlib , car Path sont des objets immuables, et vous voulez vraiment vraiment changer leur état. Avec PEP 519, vous pouvez créer votre meilleure version mutable de Path . Pour ce faire, implémentez simplement la méthode __fspath__


Toute implémentation auto-écrite de Path peut désormais fonctionner de manière native avec les fonctions intégrées Python qui attendent des chemins de fichier. Même si vous n'aimez pas pathlib , le fait de son existence est un gros plus pour les bibliothèques tierces avec leur propre Path


Mais pathlib.Path et str ne se mélangent pas, non?


Vous pensez probablement: tout cela est bien sûr génial, mais cette approche avec parfois des lignes et parfois des chemins ajoutera-t-elle de la complexité à mon code?


La réponse à cette question est oui, dans une certaine mesure. Mais ce problème a une solution de contournement assez simple.


PEP 519 a ajouté quelques autres choses en plus de PathLike : premièrement, c'est un moyen de convertir n'importe quel PathLike en chaîne, et deuxièmement, c'est un moyen de transformer n'importe quel PathLike en Path .


Prenons deux objets - une chaîne et Path (ou quoi que ce soit avec la méthode fspath ):


 from pathlib import Path import os.path p1 = os.path.join('src', 'my_package') p2 = Path('src/my_package') 

La fonction os.fspath normalise les deux objets et les transforme en chaînes:


 >>> from os import fspath >>> fspath(p1), fspath(p2) ('src/my_package', 'src/my_package') 

Dans ce cas, Path peut prendre ces deux objets dans un constructeur et les convertir en Path :


 >>> Path(p1), Path(p2) (PosixPath('src/my_package'), PosixPath('src/my_package')) 

Cela signifie que vous pouvez reconvertir le résultat de make_editorconfig en Path si nécessaire:


 >>> from pathlib import Path >>> Path(make_editorconfig(Path('src/my_package'))) PosixPath('src/my_package/.editorconfig') 

Bien sûr, la meilleure solution serait de réécrire make_editorconfig utilisant pathlib .


pathlib trop lent


J'ai vu à plusieurs reprises les performances de pathlib . C'est vrai - pathlib peut être lent. La création de milliers d'objets Path peut affecter considérablement le comportement du programme.


J'ai décidé de mesurer les performances de pathlib et os.path sur mon ordinateur à l'aide de deux programmes différents qui recherchent tous les fichiers .py dans le répertoire actuel


Voici la version d' os.walk :


 from os import getcwd, walk extension = '.py' count = 0 for root, directories, filenames in walk(getcwd()): for filename in filenames: if filename.endswith(extension): count += 1 print(f"{count} Python files found") 

Et voici la version avec Path.rglob :


 from pathlib import Path extension = '.py' count = 0 for filename in Path.cwd().rglob(f'*{extension}'): count += 1 print(f"{count} Python files found") 

Tester les performances des programmes qui fonctionnent avec le système de fichiers est une tâche délicate, car la durée de fonctionnement peut beaucoup changer. J'ai décidé d'exécuter chaque script 10 fois et j'ai comparé les meilleurs résultats pour chaque programme.


Les deux programmes ont trouvé 97507 fichiers dans le répertoire dans lequel je les ai exécutés. Le premier a fonctionné en 1,914 secondes, le second a terminé en 3,430 secondes.


Lorsque je définis le paramètre extension='' , ces programmes trouvent environ 600 000 fichiers et la différence augmente. Le premier programme a fonctionné en 1,888 secondes et le second en 7,485 secondes.


Ainsi, pathlib est environ deux fois plus lent pour les fichiers avec l'extension .py et quatre fois plus lent lorsqu'il est lancé dans mon répertoire personnel. L'écart de performances relatif entre pathlib et os est large.


Dans mon cas, cette vitesse ne change pas beaucoup. J'ai cherché tous les fichiers de mon répertoire et j'ai perdu 6 secondes. Si j'avais la tâche de traiter 10 millions de fichiers, je le réécrirais très probablement. Mais alors qu'il n'y a pas un tel besoin, vous pouvez attendre.


Si vous avez un morceau de code chaud et que pathlib affecte évidemment son fonctionnement, il n'y a rien de mal à le remplacer par une alternative. Vous ne devez pas optimiser le code, qui n'est pas un goulot d'étranglement - c'est une perte de temps supplémentaire, qui conduit également généralement à un code mal lisible, sans trop épuiser.


Amélioration de la lisibilité


Je voudrais terminer ce flux de réflexions avec quelques exemples de refactoring utilisant pathlib . J'ai pris quelques petits exemples de code qui fonctionne avec des fichiers et les pathlib fait fonctionner avec pathlib . Je laisserai la plupart du code sans commentaire sur votre terrain - décidez quelle version vous préférez.


Voici la fonction make_editorconfig nous avons vue précédemment:


 import os import os.path def make_editorconfig(dir_path): """Create .editorconfig file in given directory and return filename.""" filename = os.path.join(dir_path, '.editorconfig') if not os.path.exists(filename): os.makedirs(dir_path, exist_ok=True) open(filename, mode='wt').write('') return filename 

Et voici la version réécrite dans pathlib :


 from pathlib import Path def make_editorconfig(dir_path): """Create .editorconfig file in given directory and return filepath.""" path = Path(dir_path, '.editorconfig') if not path.exists(): path.parent.mkdir(exist_ok=True, parent=True) path.touch() return path 

Voici un programme de console qui prend une ligne avec un répertoire et imprime le contenu d'un fichier .gitignore s'il existe:


 import os.path import sys directory = sys.argv[1] ignore_filename = os.path.join(directory, '.gitignore') if os.path.isfile(ignore_filename): with open(ignore_filename, mode='rt') as ignore_file: print(ignore_file.read(), end='') 

Même chose avec pathlib :


 from pathlib import Path import sys directory = Path(sys.argv[1]) ignore_path = directory / '.gitignore' if ignore_path.is_file(): print(ignore_path.read_text(), end='') 

Voici un programme qui imprime tous les fichiers en double dans le dossier et les sous-dossiers actuels:


 from collections import defaultdict from hashlib import md5 from os import getcwd, walk import os.path def find_files(filepath): for root, directories, filenames in walk(filepath): for filename in filenames: yield os.path.join(root, filename) file_hashes = defaultdict(list) for path in find_files(getcwd()): with open(path, mode='rb') as my_file: file_hash = md5(my_file.read()).hexdigest() file_hashes[file_hash].append(path) for paths in file_hashes.values(): if len(paths) > 1: print("Duplicate files found:") print(*paths, sep='\n') 

Même chose avec c pathlib :


 from collections import defaultdict from hashlib import md5 from pathlib import Path def find_files(filepath): for path in Path(filepath).rglob('*'): if path.is_file(): yield path file_hashes = defaultdict(list) for path in find_files(Path.cwd()): file_hash = md5(path.read_bytes()).hexdigest() file_hashes[file_hash].append(path) for paths in file_hashes.values(): if len(paths) > 1: print("Duplicate files found:") print(*paths, sep='\n') 

, , -, . pathlib .


pathlib.Path


.


/ pathlib.Path . , .


 >>> path1 = Path('dir', 'file') >>> path2 = Path('dir') / 'file' >>> path3 = Path('dir/file') >>> path3 WindowsPath('dir/file') >>> path1 == path2 == path3 True 

Python (. open ) Path , , pathlib , !


 from shutil import move def rename_and_redirect(old_filename, new_filename): move(old, new) with open(old, mode='wt') as f: f.write(f'This file has moved to {new}') 

 >>> from pathlib import Path >>> old, new = Path('old.txt'), Path('new.txt') >>> rename_and_redirect(old, new) >>> old.read_text() 'This file has moved to new.txt' 

pathlib , , PathLike . , , , PEP 519 .


 >>> from plumbum import Path >>> my_path = Path('old.txt') >>> with open(my_path) as f: ... print(f.read()) ... This file has moved to new.txt 

pathlib , ( , ), , .


, pathlib . Python :


 from pathlib import Path gitignore = Path('.gitignore') if gitignore.is_file(): print(gitignore.read_text(), end='') 

pathlib — . !

Source: https://habr.com/ru/post/fr453862/


All Articles