Référence requirements.txt pour le kwarg install_requires dans le fichier setuptools setup.py


279

J'ai un requirements.txtfichier que j'utilise avec Travis-CI. Il semble stupide de dupliquer les exigences dans les deux requirements.txtet setup.py, donc j'espérais passer un install_requiresdescripteur de fichier au kwarg dans setuptools.setup.

Est-ce possible? Si oui, comment dois-je procéder?

Voici mon requirements.txtdossier:

guessit>=0.5.2
tvdb_api>=1.8.2
hachoir-metadata>=1.3.3
hachoir-core>=1.3.3
hachoir-parser>=1.3.4

4
install_requiresest utilisé pour déclarer les dépendances sur les packages qui sont nécessaires au fonctionnement du package et sont utilisées par le développeur du package, tandis qu'il requirements.txtest utilisé pour automatiser l'installation des environnements, qui permet d'installer des logiciels supplémentaires et d'effectuer l'épinglage de version et est utilisé par les administrateurs système déployant le paquet. Leur rôle et leur public cible diffèrent considérablement, donc essayer de les combiner comme le souhaite OP est une véritable erreur de conception.
Zart

7
Mes 2 cents. N'utilisez pas requirements.txt dans votre setup.py. Les finalités sont différentes, ared caremad.io/2013/07/setup-vs-requirement
Philippe Ombredanne

3
Je vois beaucoup de réponses compliquées. Quel est le problème avec plain old [line.strip() for line in open("requirements.txt").readlines()]?
Felipe SS Schneider

Il n'est pas recommandé de le faire. Mais si vraiment nécessaire, c'est simple: setuptools lui-même a déjà tout le nécessairepkg_resources.parse_requirements()
sinoroc

Réponses:


246

Vous pouvez le retourner et répertorier les dépendances dans setup.pyet avoir un seul caractère - un point .- à la requirements.txtplace.


Alternativement, même si cela n'est pas conseillé, il est toujours possible d'analyser le requirements.txtfichier (s'il ne fait référence à aucune exigence externe par URL) avec le hack suivant (testé avec pip 9.0.1):

install_reqs = parse_requirements('requirements.txt', session='hack')

Cependant, cela ne filtre pas les marqueurs d'environnement .


Dans les anciennes versions de pip, plus spécifiquement antérieures à 6.0 , il existe une API publique qui peut être utilisée pour y parvenir. Un fichier d'exigences peut contenir des commentaires ( #) et peut inclure d'autres fichiers ( --requirementou -r). Ainsi, si vous voulez vraiment analyser un, requirements.txtvous pouvez utiliser l'analyseur pip:

from pip.req import parse_requirements

# parse_requirements() returns generator of pip.req.InstallRequirement objects
install_reqs = parse_requirements(<requirements_path>)

# reqs is a list of requirement
# e.g. ['django==1.5.1', 'mezzanine==1.4.6']
reqs = [str(ir.req) for ir in install_reqs]

setup(
    ...
    install_requires=reqs
)

26
Que faire si l'utilisateur n'a pas installé pip? Ka-boom?
Gringo Suave

83
@GringoSuave Si l'utilisateur n'a pas installé pip, il doit d'abord l'installer.
guettli

7
Vous devez également fournir les URL dans votre fichier d'exigences, au cas où il y aurait des lignes -e ou -f ("git repo" modifiable) pointant vers des packages non-pypi. Utilisez ceci:setup(..., dependency_links=[str(req_line.url) for req_line in parse_requirements(<requirements_path>)], ...)
plaques de cuisson

91
Vous ne voulez vraiment pas faire ça. Parler en tant que mainteneur de pip pip ne supporte pas du tout d'être appelé comme une API comme celle-ci. En fait, pip 1.6 (la prochaine version à l'heure actuelle) déplace cette fonction.
Donald Stufft

26
Ce ne devrait plus être la réponse acceptée, si jamais elle devait l'être. Il est manifestement cassé. Même quand cela a fonctionné, c'est manifestement inutile. Comme par pipdéfaut, l'analyse des dépendances setup.pyen l'absence de requirements.txt, la réponse simple astucieusement notée par Tobu ci-dessous est de répertorier toutes les dépendances dans setup.pyet de les supprimer requirements.txt. Pour les applications nécessitant les deux, réduisez simplement la liste des dépendances requirements.txtau simple .caractère. Terminé.
Cecil Curry

195

A première vue, il ne semble que requirements.txtet setup.pysont des doublons stupides, mais il est important de comprendre que si la forme est similaire, la fonction prévue est très différente.

Le but d'un auteur de package, lors de la spécification des dépendances, est de dire "partout où vous installez ce package, ce sont les autres packages dont vous avez besoin, pour que ce package fonctionne."

En revanche, l'auteur du déploiement (qui peut être la même personne à un moment différent) a un travail différent, en ce sens qu'il dit "voici la liste des packages que nous avons rassemblés et testés et que je dois maintenant installer".

L'auteur du package écrit pour une grande variété de scénarios, car ils mettent leur travail à disposition de manière qu'ils ne connaissent peut-être pas et n'ont aucun moyen de savoir quels packages seront installés à côté de leur package. Afin d'être un bon voisin et d'éviter les conflits de versions de dépendances avec d'autres packages, ils doivent spécifier une gamme de versions de dépendances aussi large que possible. C'est ce que install_requiresdans le setup.pyfait.

L'auteur du déploiement écrit pour un objectif très différent et très spécifique: une seule instance d'une application ou d'un service installé, installé sur un ordinateur particulier. Afin de contrôler précisément un déploiement et d'être sûr que les bons packages sont testés et déployés, l'auteur du déploiement doit spécifier la version exacte et l'emplacement source de chaque package à installer, y compris les dépendances et les dépendances des dépendances. Avec cette spécification, un déploiement peut être appliqué de manière répétée à plusieurs machines, ou testé sur une machine de test, et l'auteur du déploiement peut être sûr que les mêmes packages sont déployés à chaque fois. C'est ce que requirements.txtfait un .

Vous pouvez donc voir que, bien qu'ils ressemblent tous les deux à une grande liste de packages et de versions, ces deux choses ont des tâches très différentes. Et c'est vraiment facile de mélanger cela et de se tromper! Mais la bonne façon de penser à cela est que requirements.txtc'est une "réponse" à la "question" posée par les exigences dans tous les différents setup.pyfichiers de package. Plutôt que de l'écrire à la main, il est souvent généré en disant à pip de regarder tous les setup.pyfichiers dans un ensemble de packages souhaités, de trouver un ensemble de packages qui, selon lui, répond à toutes les exigences, puis, après leur installation, "geler" "cette liste de packages dans un fichier texte (c'est de là que pip freezevient le nom).

Donc, le point à retenir:

  • setup.pydevrait déclarer les versions de dépendances les plus lâches qui sont toujours utilisables. Son travail consiste à dire avec quoi un package particulier peut fonctionner.
  • requirements.txtest un manifeste de déploiement qui définit un travail d'installation complet et ne doit pas être considéré comme lié à un seul package. Son travail consiste à déclarer une liste exhaustive de tous les packages nécessaires pour faire fonctionner un déploiement.
  • Parce que ces deux choses ont un contenu et des raisons d'exister si différents, il n'est pas possible de simplement copier l'une dans l'autre.

Références:


10
C'est l'une des meilleures explications qui me permettent de mettre de l'ordre dans ce gâchis appelé installation de packages! :)
Kounavi

6
Je ne comprends toujours pas pourquoi un développeur conserverait une version contrôlée requirements.txtavec la source du package qui contient les exigences concrètes / figées pour l'installation ou le test. Certes , setup.pypeut être utilisé à cet effet dans le projet lui - même? Je ne peux qu'imaginer utiliser un tel fichier pour les outils utilisés pour soutenir la gestion du projet (par exemple refactoring, création de versions, etc.).
Sam Brightman du

2
@samBrightman Je suis entièrement d'accord, je ne pense pas que les packages de bibliothèque ou les packages d'application devraient valider leur fichier requirements.txt dans le référentiel avec le code. Je pense que cela devrait être un artefact généré pendant les tests de build, puis utilisé pour documenter un manifeste de build et finalement générer un artefact de déploiement.
Jonathan Hanson

6
Donc, vous dites qu'il requirements.txty a plus de documentation sur l'état du monde qui a produit une construction donnée, même si elle n'est généralement pas utilisée dans le processus de construction lui-même? Ça a du sens. Cependant, il semble que plusieurs systèmes reposent sur la duplication: Travis installe certains (anciens) packages par défaut dans votre virtualenv et dit d'utiliser requirements.txt. Si je demande comment s'assurer que les dépendances sont utilisées au plus tard setup.py, les gens insistent pour que je les utilise requirements.txt.
Sam Brightman

2
Le meilleur conseil que vous pouvez tirer de tout cela est de trouver un modèle qui vous convient, de bien le documenter et de vous assurer que tous ceux avec qui vous travaillez le comprennent. Réfléchissez à la raison pour laquelle vous faites chaque bit et si cela a vraiment du sens pour votre cas d'utilisation. Et essayez de rester aussi bien informé que possible sur l'état actuel de la construction, de l'empaquetage et de la publication en Python, au cas où les choses s'amélioreraient. Mais ne retenez pas votre souffle.
Jonathan Hanson

90

Il ne peut pas prendre de descripteur de fichier. L' install_requiresargument ne peut être qu'une chaîne ou une liste de chaînes .

Vous pouvez bien sûr lire votre fichier dans le script de configuration et le transmettre sous forme de liste de chaînes à install_requires.

import os
from setuptools import setup

with open('requirements.txt') as f:
    required = f.read().splitlines()

setup(...
install_requires=required,
...)

5
Bien qu'utile, cela fait passer la spécification des exigences de déclarative à impérative. Cela rend impossible pour certains outils de connaître vos besoins. Par exemple, PyCharm propose une installation automatique de toutes les exigences spécifiées dans install_requires. Cependant, cela ne fonctionne pas si vous n'utilisez pas de syntaxe déclarative.
Piotr Dobrogost

55
@PiotrDobrogost Peut-être que le développeur PyCharm devrait alors réparer son programme. setup.pyest un programme qui doit être exécuté, pas un fichier de données qui doit être analysé. Cela n'empire pas cette réponse.
Fredrick Brennan

5
Je signale simplement d'éventuels problèmes; cette réponse est parfaitement bien. Ce n'est pas seulement PyCharm qui a un problème avec les informations "cachées" derrière le code. C'est un problème universel et il y a donc une évolution générale vers la spécification déclarative des métadonnées dans le packaging Python.
Piotr Dobrogost

33
Fonctionne très bien tant que vous mettez include requirements.txtdans votre MANIFEST.inou vous ne pourrez pas installer votre bibliothèque à partir d'une distribution source.
Pankrat

4
Je sais que c'est une vieille question, mais vous pouvez au moins de nos jours configurer PyCharm pour analyser un fichier d'exigences dans Préférences-> Outils-> Outils intégrés Python-> Fichier d'exigences de package
lekksi

64

Les fichiers d'exigences utilisent un format pip étendu, qui n'est utile que si vous avez besoin de compléter votre setup.pyavec des contraintes plus fortes, par exemple en spécifiant les URL exactes dont certaines dépendances doivent provenir, ou la sortie de pip freezepour figer l'ensemble du package en mode connu. versions. Si vous n'avez pas besoin des contraintes supplémentaires, utilisez uniquement a setup.py. Si vous sentez que vous avez vraiment besoin d'en expédier un de requirements.txttoute façon, vous pouvez en faire une seule ligne:

.

Il sera valide et fera exactement référence au contenu de celui setup.pyqui se trouve dans le même répertoire.


9
Mais dans ce cas, il essaierait également d'installer mon application. Que faire si je n'en ai pas besoin et que je souhaite uniquement installer install_requires?
fête du

2
Pour expliquer ce que @ffeast demande, si les exigences n'existent que dans setup.py, existe-t-il un moyen d'installer les exigences (équivalent de pip install -r requirements.txt ) sans installer le package lui-même?
haridsv

1
@ffeast @haridsv -e .devrait suffire. Consultez cette page: caremad.io/posts/2013/07/setup-vs-requirement
dexhunter

4
@ DexD.Hunter, il essaie toujours d'installer l'application elle-même. Ce n'est pas ce que nous voulons
fête

38

Bien que ce ne soit pas une réponse exacte à la question, je recommande le billet de blog de Donald Stufft à https://caremad.io/2013/07/setup-vs-requirement/ pour une bonne interprétation de ce problème. Je l'utilise avec beaucoup de succès.

Bref, ce requirements.txtn'est pas une setup.pyalternative, mais un complément de déploiement. Conservez une abstraction appropriée des dépendances des packages dans setup.py. Définissez un requirements.txtou plusieurs d'entre eux pour récupérer des versions spécifiques des dépendances de package pour le développement, les tests ou la production.

Par exemple, avec des packages inclus dans le repo sous deps/:

# fetch specific dependencies
--no-index
--find-links deps/

# install package
# NOTE: -e . for editable mode
.

pip exécute les packages setup.pyet installe les versions spécifiques des dépendances déclarées dans install_requires. Il n'y a aucune duplicité et le but des deux artefacts est préservé.


7
Cela ne fonctionne pas lorsque vous souhaitez fournir un package pour les autres à installer via pip install my-package. Si les dépendances de my-package ne sont pas répertoriées dans my-package / setup.py, elles ne sont pas installées par pip install my-package. Je n'ai pas pu déterminer comment fournir un package pour d'autres qui inclut des dépendances sans les indiquer explicitement dans setup.py. J'aimerais savoir si quelqu'un a compris comment le garder au sec tout en permettant aux autres d'installer les dépendances my-package + sans télécharger le fichier d'exigences et appeler manuellement pip install -r my-package/requirements.txt.
Malina

2
@Malina Le package ici est parfaitement installable sans requirements.txt. Exactement. Mise à jour de la question pour clarifier les choses. Lien de mise à jour du blog obsolète également mis à jour.
célèbregarkin

donc lors de l'exécution de setup.py, il appellera requirements.txt pour des versions spécifiques des fichiers répertoriés dans stup.py?
dtracers

C'est l'inverse @dtracers. requirements.txt pointe vers le package lui-même, où les dépendances de setup.py peuvent être récupérées. Donc, lors de l'installation à l'aide d'exigences, cela fonctionne et lors de l'installation via pip, cela fonctionne aussi - dans les deux cas en utilisant les dépendances de setup.py, mais aussi en permettant d'installer plus de choses lors de l'utilisation de requirements.txt
smido

20

L'utilisation parse_requirementsest problématique car l'API pip n'est pas documentée et prise en charge publiquement. Dans le pip 1.6, cette fonction est en train de se déplacer, de sorte que les utilisations existantes de celle-ci risquent de se casser.

Un moyen plus fiable pour éliminer la duplication entre setup.pyet requirements.txtconsiste à spécifier vos dépendances dans setup.py, puis à les placer -e .dans votre requirements.txtfichier. Certaines informations d'un des pipdéveloppeurs sur les raisons pour lesquelles c'est une meilleure façon de procéder sont disponibles ici: https://caremad.io/blog/setup-vs-requirement/


@Tommy Essayez ceci: caremad.io/2013/07/setup-vs-requirement Il s'agit du même lien que celui publié dans une autre réponse.
Amit

18

La plupart des autres réponses ci-dessus ne fonctionnent pas avec la version actuelle de l'API de pip. Voici la façon * correcte de le faire avec la version actuelle de pip (6.0.8 au moment de la rédaction, fonctionnait également en 7.1.2. Vous pouvez vérifier votre version avec pip -V).

from pip.req import parse_requirements
from pip.download import PipSession

install_reqs = parse_requirements(<requirements_path>, session=PipSession())

reqs = [str(ir.req) for ir in install_reqs]

setup(
    ...
    install_requires=reqs
    ....
)

* Correct, en ce sens que c'est la façon d'utiliser parse_requirements avec le pip actuel. Ce n'est probablement pas la meilleure façon de le faire, car, comme l'ont indiqué les affiches ci-dessus, pip ne maintient pas vraiment une API.


14

Installez le package actuel dans Travis. Cela évite l'utilisation d'un requirements.txtfichier. Par exemple:

language: python
python:
  - "2.7"
  - "2.6"
install:
  - pip install -q -e .
script:
  - python runtests.py

2
C'est de loin la meilleure combinaison de "correct" et de "pratique". J'ajouterais que si après les tests, vous pouvez demander à Travis de générer un requirements.txt avec pip freezeet d'exporter ce fichier quelque part comme un artefact (comme S3 ou quelque chose), alors vous auriez un excellent moyen d'installer de manière répétitive exactement ce que vous testé.
Jonathan Hanson

4

from pip.req import parse_requirements n'a pas fonctionné pour moi et je pense que c'est pour les lignes vides dans mon requirements.txt, mais cette fonction fonctionne

def parse_requirements(requirements):
    with open(requirements) as f:
        return [l.strip('\n') for l in f if l.strip('\n') and not l.startswith('#')]

reqs = parse_requirements(<requirements_path>)

setup(
    ...
    install_requires=reqs,
    ...
)

4

Si vous ne voulez pas forcer vos utilisateurs à installer pip, vous pouvez émuler son comportement avec ceci:

import sys

from os import path as p

try:
    from setuptools import setup, find_packages
except ImportError:
    from distutils.core import setup, find_packages


def read(filename, parent=None):
    parent = (parent or __file__)

    try:
        with open(p.join(p.dirname(parent), filename)) as f:
            return f.read()
    except IOError:
        return ''


def parse_requirements(filename, parent=None):
    parent = (parent or __file__)
    filepath = p.join(p.dirname(parent), filename)
    content = read(filename, parent)

    for line_number, line in enumerate(content.splitlines(), 1):
        candidate = line.strip()

        if candidate.startswith('-r'):
            for item in parse_requirements(candidate[2:].strip(), filepath):
                yield item
        else:
            yield candidate

setup(
...
    install_requires=list(parse_requirements('requirements.txt'))
)

4

L'interface suivante est devenue obsolète dans pip 10:

from pip.req import parse_requirements
from pip.download import PipSession

Je l'ai donc commuté juste en simple analyse de texte:

with open('requirements.txt', 'r') as f:
    install_reqs = [
        s for s in [
            line.split('#', 1)[0].strip(' \t\n') for line in f
        ] if s != ''
    ]

Cette approche simple fonctionne plus de 90% du temps. Pour ceux qui utilisent Python 3.6+, j'ai écrit une réponse qui en est une pathlibvariante .
Acumenus

3

Cette approche simple lit le fichier des exigences à partir de setup.py. Il s'agit d'une variante de la réponse de Dmitiry S .. Cette réponse est compatible uniquement avec Python 3.6+.

Par DS , requirements.txtpeut documenter des exigences concrètes avec des numéros de version spécifiques, tandis que setup.pypeut documenter des exigences abstraites avec des plages de versions lâches.

Ci-dessous est un extrait de mon setup.py.

import distutils.text_file
from pathlib import Path
from typing import List

def _parse_requirements(filename: str) -> List[str]:
    """Return requirements from requirements file."""
    # Ref: https://stackoverflow.com/a/42033122/
    return distutils.text_file.TextFile(filename=str(Path(__file__).with_name(filename))).readlines()

setup(...
      install_requires=_parse_requirements('requirements.txt'),
   ...)

Notez que distutils.text_file.TextFilecela supprimera les commentaires. De plus, d'après mon expérience, vous n'avez apparemment pas besoin de prendre de mesure spéciale pour regrouper le fichier des exigences.


2

ATTENTION AU parse_requirementsCOMPORTEMENT!

Veuillez noter que pip.req.parse_requirementsles traits de soulignement seront remplacés par des tirets. Cela me rendait furieux pendant quelques jours avant de le découvrir. Exemple démontrant:

from pip.req import parse_requirements  # tested with v.1.4.1

reqs = '''
example_with_underscores
example-with-dashes
'''

with open('requirements.txt', 'w') as f:
    f.write(reqs)

req_deps = parse_requirements('requirements.txt')
result = [str(ir.req) for ir in req_deps if ir.req is not None]
print result

produit

['example-with-underscores', 'example-with-dashes']

1
Utilisez unsafe_name pour obtenir la version soulignée:[ir.req.unsafe_name for ir in req_deps if ir.req is not None]
alanjds

5
Comme indiqué ailleurs, PIP est une application, pas une bibliothèque. Il n'a pas d'API acceptée publiquement et son importation dans votre code n'est pas un cas d'utilisation pris en charge. Il n'est pas surprenant qu'il ait un comportement inattendu; ses fonctions internes n'ont jamais été destinées à être utilisées de cette façon.
Jonathan Hanson

1

J'ai créé une fonction réutilisable pour cela. Il analyse en fait tout un répertoire de fichiers d'exigences et les définit sur extras_require.

Le dernier toujours disponible ici: https://gist.github.com/akatrevorjay/293c26fefa24a7b812f5

import glob
import itertools
import os

# This is getting ridiculous
try:
    from pip._internal.req import parse_requirements
    from pip._internal.network.session import PipSession
except ImportError:
    try:
        from pip._internal.req import parse_requirements
        from pip._internal.download import PipSession
    except ImportError:
        from pip.req import parse_requirements
        from pip.download import PipSession


def setup_requirements(
        patterns=[
            'requirements.txt', 'requirements/*.txt', 'requirements/*.pip'
        ],
        combine=True):
    """
    Parse a glob of requirements and return a dictionary of setup() options.
    Create a dictionary that holds your options to setup() and update it using this.
    Pass that as kwargs into setup(), viola

    Any files that are not a standard option name (ie install, tests, setup) are added to extras_require with their
    basename minus ext. An extra key is added to extras_require: 'all', that contains all distinct reqs combined.

    Keep in mind all literally contains `all` packages in your extras.
    This means if you have conflicting packages across your extras, then you're going to have a bad time.
    (don't use all in these cases.)

    If you're running this for a Docker build, set `combine=True`.
    This will set `install_requires` to all distinct reqs combined.

    Example:

    >>> import setuptools
    >>> _conf = dict(
    ...     name='mainline',
    ...     version='0.0.1',
    ...     description='Mainline',
    ...     author='Trevor Joynson <github@trevor.joynson,io>',
    ...     url='https://trevor.joynson.io',
    ...     namespace_packages=['mainline'],
    ...     packages=setuptools.find_packages(),
    ...     zip_safe=False,
    ...     include_package_data=True,
    ... )
    >>> _conf.update(setup_requirements())
    >>> # setuptools.setup(**_conf)

    :param str pattern: Glob pattern to find requirements files
    :param bool combine: Set True to set install_requires to extras_require['all']
    :return dict: Dictionary of parsed setup() options
    """
    session = PipSession()

    # Handle setuptools insanity
    key_map = {
        'requirements': 'install_requires',
        'install': 'install_requires',
        'tests': 'tests_require',
        'setup': 'setup_requires',
    }
    ret = {v: set() for v in key_map.values()}
    extras = ret['extras_require'] = {}
    all_reqs = set()

    files = [glob.glob(pat) for pat in patterns]
    files = itertools.chain(*files)

    for full_fn in files:
        # Parse
        reqs = {
            str(r.req)
            for r in parse_requirements(full_fn, session=session)
            # Must match env marker, eg:
            #   yarl ; python_version >= '3.0'
            if r.match_markers()
        }
        all_reqs.update(reqs)

        # Add in the right section
        fn = os.path.basename(full_fn)
        barefn, _ = os.path.splitext(fn)
        key = key_map.get(barefn)

        if key:
            ret[key].update(reqs)
            extras[key] = reqs

        extras[barefn] = reqs

    if 'all' not in extras:
        extras['all'] = list(all_reqs)

    if combine:
        extras['install'] = ret['install_requires']
        ret['install_requires'] = list(all_reqs)

    def _listify(dikt):
        ret = {}

        for k, v in dikt.items():
            if isinstance(v, set):
                v = list(v)
            elif isinstance(v, dict):
                v = _listify(v)
            ret[k] = v

        return ret

    ret = _listify(ret)

    return ret


__all__ = ['setup_requirements']

if __name__ == '__main__':
    reqs = setup_requirements()
    print(reqs)

très agréable! gère même les exigences récursives avec le dernier pip :)
amohr

@amohr Merci! Je l'ai récemment mis à jour pour un pip encore plus tard, je ne sais pas pourquoi ils agissent comme ils le font, en déplaçant les choses vers pip._internal... Si vous ne fournissez pas d'API externe utilisable, vous ne devriez pas casser tous ceux qui utilisent tout ce que vous fournissez.
trevorj

0

Une autre solution possible ...

def gather_requirements(top_path=None):
    """Captures requirements from repo.

    Expected file format is: requirements[-_]<optional-extras>.txt

    For example:

        pip install -e .[foo]

    Would require:

        requirements-foo.txt

        or

        requirements_foo.txt

    """
    from pip.download import PipSession
    from pip.req import parse_requirements
    import re

    session = PipSession()
    top_path = top_path or os.path.realpath(os.getcwd())
    extras = {}
    for filepath in tree(top_path):
        filename = os.path.basename(filepath)
        basename, ext = os.path.splitext(filename)
        if ext == '.txt' and basename.startswith('requirements'):
            if filename == 'requirements.txt':
                extra_name = 'requirements'
            else:
                _, extra_name = re.split(r'[-_]', basename, 1)
            if extra_name:
                reqs = [str(ir.req) for ir in parse_requirements(filepath, session=session)]
                extras.setdefault(extra_name, []).extend(reqs)
    all_reqs = set()
    for key, values in extras.items():
        all_reqs.update(values)
    extras['all'] = list(all_reqs)
    return extras

et ensuite utiliser ...

reqs = gather_requirements()
install_reqs = reqs.pop('requirements', [])
test_reqs = reqs.pop('test', [])
...
setup(
    ...
    'install_requires': install_reqs,
    'test_requires': test_reqs,
    'extras_require': reqs,
    ...
)

d'où treevient-il?
Francesco Boi

@FrancescoBoi si vous me pardonnez un peu de ne pas avoir présenté de solution pleinement fonctionnelle ... l'arborescence n'est en réalité qu'une analyse du système de fichiers local (très similaire à une commande "tree" sous linux). De plus, ma solution ci-dessus peut ne pas fonctionner entièrement à ce stade car pip est constamment mis à jour et j'ai utilisé les internes de pip.
Brian Bruggeman

0

Je ne recommanderais pas de faire une telle chose. Comme mentionné plusieurs fois install_requireset ne requirements.txtsont certainement pas censés être la même liste. Mais comme il y a beaucoup de réponses trompeuses tout autour impliquant des API internes privées de pip , il pourrait être utile de chercher des alternatives plus sûres ...

Il n'est pas nécessaire que pip analyse un requirements.txtfichier à partir d'un script setuptools setup.py . Le projet setuptools contient déjà tous les outils nécessaires dans son package de niveau supérieurpkg_resources .

Cela pourrait plus ou moins ressembler à ceci:

#!/usr/bin/env python3

import pathlib

import pkg_resources
import setuptools

with pathlib.Path('requirements.txt').open() as requirements_txt:
    install_requires = [
        str(requirement)
        for requirement
        in pkg_resources.parse_requirements(requirements_txt)
    ]

setuptools.setup(
    install_requires=install_requires,
)

Au cas où vous ne seriez pas au courant, la raison pour laquelle beaucoup (moi y compris) ont utilisé pipl'analyse syntaxique et non pkg_resourcesdepuis avant 2015 sont des bogues tels que github.com/pypa/setuptools/issues/470 . Celui-ci est corrigé de nos jours, mais j'ai toujours un peu peur de l'utiliser, car les deux implémentations semblent être développées séparément.
trevorj

@trevorj Merci d'avoir signalé cela, je ne savais pas. Le fait est que cela fonctionne de nos jours et s'impliquer est une idée ridicule pour moi (en particulier de cette façon). Jetez un oeil sur les autres réponses, la plupart semblent être de légères variations de la même idée mal avisée, sans guère d'avertissement. Et les nouveaux arrivants pourraient bien suivre cette tendance. Espérons que des initiatives telles que PEP517 et PEP518 éloigneront la communauté de cette folie.
sinoroc

-1

Cross poster ma réponse de cette question SO pour une autre solution simple, à l'épreuve de la version pip.

try:  # for pip >= 10
    from pip._internal.req import parse_requirements
    from pip._internal.download import PipSession
except ImportError:  # for pip <= 9.0.3
    from pip.req import parse_requirements
    from pip.download import PipSession

requirements = parse_requirements(os.path.join(os.path.dirname(__file__), 'requirements.txt'), session=PipSession())

if __name__ == '__main__':
    setup(
        ...
        install_requires=[str(requirement.req) for requirement in requirements],
        ...
    )

Ensuite, ajoutez simplement toutes vos exigences sous le requirements.txtrépertoire racine du projet.


-1

J'ai fait ça:

import re

def requirements(filename):
    with open(filename) as f:
        ll = f.read().splitlines()
    d = {}
    for l in ll:
        k, v = re.split(r'==|>=', l)
        d[k] = v
    return d

def packageInfo():
    try:
        from pip._internal.operations import freeze
    except ImportError:
        from pip.operations import freeze

    d = {}
    for kv in freeze.freeze():
        k, v = re.split(r'==|>=', kv)
        d[k] = v
    return d

req = getpackver('requirements.txt')
pkginfo = packageInfo()

for k, v in req.items():
    print(f'{k:<16}: {v:<6} -> {pkginfo[k]}')

-2

Encore un autre parse_requirementshack qui analyse également les marqueurs d'environnement en extras_require:

from collections import defaultdict
from pip.req import parse_requirements

requirements = []
extras = defaultdict(list)
for r in parse_requirements('requirements.txt', session='hack'):
    if r.markers:
        extras[':' + str(r.markers)].append(str(r.req))
    else:
        requirements.append(str(r.req))

setup(
    ...,
    install_requires=requirements,
    extras_require=extras
)

Il devrait prendre en charge les distances sdist et binaires.

Comme indiqué par d'autres, parse_requirementsprésente plusieurs lacunes, ce n'est donc pas ce que vous devez faire sur des projets publics, mais cela peut suffire pour des projets internes / personnels.


pip 20.1 a changé son API et les marqueurs ne sont plus disponibles via parse_requirements(), donc cela échoue maintenant.
Tuukka Mustonen

-3

Voici un hack complet (testé avec pip 9.0.1) basé sur la réponse de Romain qui l'analyse requirements.txtet le filtre en fonction des marqueurs d'environnement actuels :

from pip.req import parse_requirements

requirements = []
for r in parse_requirements('requirements.txt', session='hack'):
    # check markers, such as
    #
    #     rope_py3k    ; python_version >= '3.0'
    #
    if r.match_markers():
        requirements.append(str(r.req))

print(requirements)

1
Ce n'est que partiellement vrai. Si vous appelez, r.match_markers()vous êtes en train d'évaluer les marqueurs, ce qui est correct pour un sdist. Cependant, si vous construisez une dist binaire (par exemple une roue), le package ne listera que les bibliothèques qui correspondent à votre environnement de construction.
Tuukka Mustonen

@TuukkaMustonen, alors où trouver cela wheel environment(si c'est la chose que la personne essaie de faire) pour évaluer les marqueurs par rapport à cela?
anatoly techtonik

Voir stackoverflow.com/a/41172125/165629 qui devrait également prendre en charge bdist_wheel. Il n'évalue pas les marqueurs, il les ajoute simplement extras_require.
Tuukka Mustonen
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.