Partage
  • Partager sur Facebook
  • Partager sur Twitter

Erreur de relative import avec l'option -m

    10 novembre 2023 à 21:39:38

    Salut tout le monde !

    J'ai un dossier avec un fichier foo.py et un fichier bar.py :

    foo.py

    from .bar import x
    
    print(f'got from bar: {x=}')

    bar.py

    x = 1

    Lorsque je tape python -m foo

    J'ai cette erreur :

    Traceback (most recent call last):
      File "/usr/lib/python3.8/runpy.py", line 194, in _run_module_as_main
        return _run_code(code, main_globals, None,
      File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
        exec(code, run_globals)
      File "/home/user/a/b/c/foo.py", line 1, in <module>
        from .bar import x
    ImportError: attempted relative import with no known parent package

    Lorsque je tape python foo.py

    J'ai cette erreur :

    Traceback (most recent call last):
      File "foo.py", line 1, in <module>
        from .bar import x
    ImportError: attempted relative import with no known parent package

    Pourtant lorsque je me place dans le répertoire parent puis que je tape : python -m c.foo

    Là mon script fonctionne !

    got from bar: x=1

    Quelqu'un pourrait m'aider à comprendre pourquoi ?

    J'ai vraiment passé des heures à essayer de comprendre comment les relative imports fonctionnent avec Python mais c'est vraiment dur de trouver de bonnes infos... Pourtant avec tous les topics sur le sujet je vois bien que je ne suis pas le seul à galérer avec ça...

    Merci d'avance


    -
    Edité par ThomasAirain 10 novembre 2023 à 21:41:37

    • Partager sur Facebook
    • Partager sur Twitter
      11 novembre 2023 à 7:38:31

      python -m c.foo crée la racine dont le .bar (import relatif mais à quoi) à besoin pour être résolu (comme le mentionnent les messages d'erreurs).

      Pour le pourquoi, c'est une construction humaine faite pour fonctionner comme ça et pas autrement. Du coup, ouvrir la documentation et essayer de comprendre ce que ça dit dans les différents cas de figure qu'on imagine est la seule démarche possible.

      • Partager sur Facebook
      • Partager sur Twitter
        11 novembre 2023 à 13:18:29

        Merci pour ta réponse !

        J'ai cherché longtemps mais je n'ai pas trouvé beaucoup de réponse dans la doc.

        -m

        https://docs.python.org/3/using/cmdline.html#cmdoption-m

        Le relative import

        https://docs.python.org/3/reference/import.html#package-relative-imports

        Par contre ici il y a plus d'info :

        https://peps.python.org/pep-0366

        Mais en gros, il faut comprendre que les relative imports ne sont pas possibles si on exécute le script depuis le répertoire qui contient le script parce que le relative import dépend de la variable __package__. Et comme __package__ est toujours None lorsqu'on exécute le script sans l'option -m et qu'il est une empty string lorsqu'on exécute le script avec -m depuis le répertoire du script, on est obligé d'exécuter le script avec à la fois l'option -m tout en étant dans un répertoire un niveau au dessus au minimum.

        OU ALORS, il faut redéfinir __package__ mais je commence à avoir une migraine à chercher comment gérer les imports correctement en Python.

        -
        Edité par ThomasAirain 11 novembre 2023 à 13:19:32

        • Partager sur Facebook
        • Partager sur Twitter
          11 novembre 2023 à 13:51:39

          ThomasAirain a écrit:

          Mais en gros, il faut comprendre que les relative imports ne sont pas possibles si on exécute le script depuis le répertoire qui contient le script parce que le relative import dépend de la variable __package__.

          Si un import est relatif, il faut comprendre par rapport à quoi. Et si on lance un module qui contient un import relatif en tant que script principal, on se prend un "attempted relative import with no known parent package".

          ThomasAirain a écrit:

          mais je commence à avoir une migraine à chercher comment gérer les imports correctement en Python.

          Il y a les fonctionnalités de bases qu'on va utiliser tout le temps et les trucs avancés qui servent parfois et qu'on peut approfondir si nécessaire. Ceci dit, tout ça c'est fait pour faire le rangement de bouts de code dans une hiérarchie de fichiers.... et ce n'est pas la méthode de rangement qui changera beaucoup les fonctionnalités à écrire. Donc sauf à écrire un cours, le sujet ne mérite pas de se prendre la tête.
          • Partager sur Facebook
          • Partager sur Twitter
            11 novembre 2023 à 17:15:52

            mps a écrit:

            Si un import est relatif, il faut comprendre par rapport à quoi. Et si on lance un module qui contient un import relatif en tant que script principal, on se prend un "attempted relative import with no known parent package".

            Pourtant si je tape python -m c.foo le script s'exécute bien avec un import relatif


            mps a écrit:

                        Il y a les fonctionnalités de bases qu'on va utiliser tout le temps et les trucs avancés qui servent parfois et qu'on peut approfondir si nécessaire. Ceci dit, tout ça c'est

                        fait pour faire le rangement de bouts de code dans une hiérarchie de fichiers.... et ce n'est pas la méthode de rangement qui changera beaucoup les              fonctionnalités à écrire. Donc sauf à écrire un cours, le sujet ne mérite pas de se prendre la tête.

            Je m'intéresse aux relative imports parce que j'ai un souci sur un projet que j'ai créé.

            L'arborescence est celle-ci :

            monprojet
            ├── sources
            └── test

            Pour faire fonctionner 'monprojet' j'ai juste à avoir le path de 'monprojet' dans le PYTHONPATH.

            Mais dans le dossier test j'ai un problème. J'ai une TestSuite qui utilise unittest et mes TestCases utilisent bien sûr le contenu dans sources vu que ce sont des tests sur mon projet. Du coup pour avoir le path de 'monprojet' dans mes tests j'ai 2 solutions mais aucune n'est élégante et aucune ne me convient vraiment :

            • Je lance les tests avec une commande à l'intérieur du répertoire 'monprojet' pour que le chemin de 'monprojet' soit automatiquement ajouté par Python dans le PYTHONPATH à l'exécution. Cette solution ne marche pas, de 1 je trouve ça embêtant de ne pouvoir lancer les tests que depuis un endroit précis du projet, de 2 mes TestCases ont besoin d'importer des fonctions depuis un module utils.py dont le chemin est monprojet/test/utils.py or cette localisation n'est pas dans le PYTHONPATH donc les imports vers cet endroit font planter mes tests
            • Je hardcode des instructions sys.path.append(LE_CHEMIN_QUI_VA_BIEN) dans tous mes fichiers TestCase et je fais des imports absolus des packages sources et test pour avoir tous les imports dont j'ai besoin. Cette solution fonctionne mais derrière je dois ajouter des sys.path.append dans tous les fichiers avec des imports. :'( C'est long, chiant, pas facile à maintenir et puis ça ressemble beaucoup à un antipattern :(

            -
            Edité par ThomasAirain 11 novembre 2023 à 17:17:21

            • Partager sur Facebook
            • Partager sur Twitter
              11 novembre 2023 à 17:39:43

              Plein de sources sont publiées sur PyPI peuvent être source d'inspiration mais si vous voulez trouver une solution originale, effectivement, il y a du boulot. Déjà il y a des différences entre tests unitaires, tests fonctionnels et côté tests fonctionnels, une application ne vole pas pareil qu'une bibliothèque. Donc pour faire une solution générique, c'est râpé.

              De toutes façons, il n'est pas interdit de créer un script d'initialisation de variables du shell pour retomber sur ses pattes... et que les imports soient absolus ou relatifs ne change pas grand chose. Le but étant d'avoir un environnement facile à construire où on peut passer des tests. Et si vous voulez mieux, notez ce qui vous choque et travaillez dessus pour tester des solutions pour le prochaine projet python.

              ThomasAirain a écrit:

              Pourtant si je tape python -m c.foo le script s'exécute bien avec un import relatif

              C'est un des intérêts de python -m...  encore heureux que ça marche.

              -
              Edité par mps 11 novembre 2023 à 17:51:56

              • Partager sur Facebook
              • Partager sur Twitter
                12 novembre 2023 à 19:13:49

                Merci pour ta réponse !

                "C'est un des intérêts de python -m...  encore heureux que ça marche." -> Pourquoi les développeurs de Python n'ont pas fait en sorte que cela fonctionne à chaque fois alors ? Ou un moyen pour que cela fonctionne même avec un script.

                J'ai regardé une bonne vingtaine de projets sur Pypi et Github mais je n'arrive pas à trouver un projet qui soit exactement dans le même cas que le mien c'est-à-dire un projet simple en Python avec un dossier test séparé des sources avec unittest (et pas pytest).

                J'ai regardé les comptes GitHub d'élèves de mon école pour voir leurs projets Python mais aucun n'a été confronté à mon problème.

                Cette stratégie de s'inspirer d'un autre projet peut paraître bonne mais en réalité si on ne connaît pas un projet avec une philosophie similaire au nôtre ça ne fonctionne pas. Et Python n'est pas mon langage principal donc je n'ai pas énormément d'expérience dessus.

                Sinon je suis tombé sur cet article : https://python-guide-pt-br.readthedocs.io/fr/latest/writing/structure.html qui semble répondre à ma question en conseillant de créer un fichier config.py dans le dossier test avec ce code :

                import os
                import sys
                sys.path.insert(0, os.path.abspath('..'))
                
                import sample

                Et ensuite d'ajouter ce code dans les tests individuels :

                from .context import sample

                Malheureusement, l'article n'explique pas ce qu'est context.

                J'ai essayé de créer mon fichier config.py avec ce code :

                import os
                import sys
                sys.path.insert(0, os.path.abspath('..'))
                
                import sources

                Puis de rajouter ce code dans mes tests individuels :

                from .config import sources


                Mais je me retrouve une nouvelle fois avec l'erreur :

                ValueError: attempted relative import beyond top-level package

                Je vois que sur Stackoverflow je suis pas le seul à avoir eu des problèmes pour importer des fichiers :

                https://stackoverflow.com/questions/14132789/relative-imports-for-the-billionth-time

                https://stackoverflow.com/questions/16981921/relative-imports-in-python-3

                https://stackoverflow.com/questions/72852/how-can-i-do-relative-imports-in-python

                Je continue de chercher mais je me demande si les imports fonctionnent vraiment en Python... Ou alors il faut configurer le sys.path dans tous les fichiers qui ne sont pas à la racine mais c'est vraiment pas génial... :'(

                -
                Edité par ThomasAirain 12 novembre 2023 à 19:16:20

                • Partager sur Facebook
                • Partager sur Twitter
                  12 novembre 2023 à 23:06:48

                  ThomasAirain a écrit:

                  Malheureusement, l'article n'explique pas ce qu'est context.

                  Il suffit d'aller farfouiller le source sur GitHub pour voir que context.py est ce qu'ils appellent config.py

                  ThomasAirain a écrit:

                  Pourquoi les développeurs de Python n'ont pas fait en sorte que cela fonctionne à chaque fois alors ?

                  l'outil fait ce pourquoi il a été conçu, mais si vous en attendez autre chose soit vous utilisez le mauvais outil soit vous l'utilisez mal.

                  ThomasAirain a écrit:

                  Cette stratégie de s'inspirer d'un autre projet peut paraître bonne mais en réalité si on ne connaît pas un projet avec une philosophie similaire au nôtre ça ne fonctionne pas.

                  Quand on recopie du code sur Internet, soit on peut l'utiliser sans avoir à le modifier soit il va falloir avoir un niveau similaire à ceux qui l'ont écrit pour le faire évoluer sans soucis. Dit a autrement, si vous débutez vous ne savez peut être pas organiser votre projet ni jauger de la pertinence de ce qui a été fait par d'autres: vous en êtes réduit à appliquer en espérant comprendre un jour.
                  • Partager sur Facebook
                  • Partager sur Twitter
                    13 novembre 2023 à 0:44:05

                    Ah oui bien vu. Je n'avais pas vu qu'il y avait un dépôt GitHub associé.

                    Malheureusement la solution du tuto ne me sert en rien car je n'ai qu'un fichier à exécuter dans le répertoire test. Donc je n'ai pas besoin de faire un fichier spécifique. Autant mettre la modification du sys.path directement dans mon unique fichier. Ensuite j'ai besoin d'avoir le bon sys.path dans l'arborescence du dossier test mais ce cas-là n'est pas prévu par le tuto.

                    Je sais que j'ai l'air d'un noob à ne pas savoir comment faire des import (pour l'instant j'ai ajouté le sys.path manuellement dans tous les fichiers pour que ça fonctionne quand même en attendant) mais en même temps comment deviner ce que les développeurs de Python ont prévu pour les imports si ce n'est pas indiqué quelque part... Ou alors je n'ai pas trouvé la bonne source.

                    • Partager sur Facebook
                    • Partager sur Twitter
                      17 novembre 2023 à 8:48:50

                      > Pourquoi les développeurs de Python n'ont pas fait en sorte que cela fonctionne à chaque fois alors ? Ou un moyen pour que cela fonctionne même avec un script.

                      Pour fonctionner l'option -m a besoin d'identifier le paquet en cours. Si tu es dans le répertoire c et exécutes python -m foo, il n'y a pas de paquet à identifier (foo est un module et non un package) donc ça ne pourra pas fonctionner.

                      Par contre effectivement, si tu te places dans le répertoire parent et exécutes python -m c.foo, là Python va être en mesure d'identifier que le paquet est c et que tu veux exécuter le module foo de ce paquet. Comme le paquet est identifié, les imports relatifs peuvent fonctionner.

                      Pour revenir à ton problème plus concret, c'est donc le répertoire source qui pourrait avoir cette fonction. Maintenant, j'imagine que tu ne veux pas avoir des from source import ... partout dans les tests et préférerait utiliser le nom monprojet.

                      La solution pour cela est de construire un paquet pip. Cela se fait à l'aide d'un fichier pyproject.toml qui définit comment construire le paquet à partir des sources : https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html

                      Une fois cette description écrite, tu peux avoir un environnement virtuel dans lequel tu exécuterais pip install -e . (le -e sert à installer en mode éditable, c'est-à-dire que les changements sur les fichiers locaux se répercuteront sur le paquet installé) pour installer le paquet défini dans le répertoire courant. Ainsi, tu auras dans ton environnement un paquet monprojet connu par Python, python -m monprojet.foo fonctionnera bien quel que soit le répertoire dans lequel tu te trouves (pour peu que l'environnement virtuel soit activé) et tu pourras faire des from monprojet import ... dans tes tests.

                      • Partager sur Facebook
                      • Partager sur Twitter
                        17 novembre 2023 à 14:35:26

                        entwanne a écrit:

                        Par contre effectivement, si tu te places dans le répertoire parent et exécutes python -m c.foo, là Python va être en mesure d'identifier que le paquet est c et que tu veux exécuter le module foo de ce paquet. Comme le paquet est identifié, les imports relatifs peuvent fonctionner.

                        Dans la pratique, foo sera aussi appelé en tant que script principal mais avec l'ajout du répertoire c dans sys.path (et parent de foo)

                        • Partager sur Facebook
                        • Partager sur Twitter

                        Erreur de relative import avec l'option -m

                        × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                        • Editeur
                        • Markdown