web-dev-qa-db-fra.com

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

J'ai un fichier requirements.txt que j'utilise avec Travis-CI. Il semble ridicule de dupliquer les exigences à la fois dans requirements.txt et setup.py, alors j'espérais passer un descripteur de fichier au install_requires kwarg dans setuptools.setup.

Est-ce possible? Si oui, comment devrais-je m'y prendre?

Voici mon fichier requirements.txt:

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

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


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

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

Cela ne filtre pas marqueurs d'environnement bien.


Dans les anciennes versions de pip, plus spécifiquement antérieure à 6. , une API publique peut être utilisée pour atteindre cet objectif. Un fichier d'exigences peut contenir des commentaires (#) et peut inclure d'autres fichiers (--requirement ou -r). Ainsi, si vous voulez vraiment analyser un requirements.txt, vous 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
)
224
Romain Hardouin

À première vue, il semble bien que requirements.txt et setup.py soient des duplicata idiots, mais il est important de comprendre que même si la forme est similaire, la fonction recherchée est très différente.

Le but d'un auteur de paquet, lors de la spécification de dépendances, est de dire "où que vous installiez ce paquet, ce sont les autres paquets dont vous avez besoin pour que ce paquet fonctionne."

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

L’auteur du paquet écrit pour une grande variété de scénarios, parce qu’il met son travail à disposition d’une manière dont il ne connaît peut-être pas l’information et n’a aucun moyen de savoir quels paquets seront installés à côté de son paquet. Afin d’être un bon voisin et d’éviter les conflits de versions de dépendance avec d’autres packages, ils doivent spécifier le plus grand nombre possible de versions de dépendance. C’est ce que install_requires dans setup.py fait.

L'auteur du déploiement écrit pour un objectif très différent, très spécifique: une instance unique d'une application ou d'un service installé, installé sur un ordinateur particulier. Pour contrôler avec précision un déploiement et s'assurer que les packages appropriés 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. 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 assuré que les mêmes packages sont déployés à chaque fois. C'est ce que fait un requirements.txt.

Vous voyez donc que, bien qu'ils ressemblent tous les deux à une longue liste de packages et de versions, ces deux tâches ont des tâches très différentes. Et il est vraiment facile de mélanger cela et de se tromper! Mais la bonne façon de penser à cela est que requirements.txt est une "réponse" à la "question" posée par les exigences de tous les divers fichiers de package setup.py. Plutôt que de l'écrire à la main, il est souvent généré en demandant à pip de regarder tous les fichiers setup.py dans un ensemble de packages souhaités, de rechercher un ensemble de packages qu'il juge adaptés à toutes les exigences, puis après. Réinstallé, "gèle" cette liste de paquets dans un fichier texte (c’est de là que vient le nom pip freeze).

Donc la livraison:

  • setup.py devrait déclarer les versions de dépendance les plus lâches possibles et toujours utilisables. Son travail consiste à dire avec quoi un paquet particulier peut fonctionner.
  • requirements.txt est 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 au bon fonctionnement d'un déploiement.
  • Étant donné que ces deux éléments ont un contenu et des raisons d'exister si différents, il est impossible de simplement copier l'un dans l'autre.

Références:

145
Jonathan Hanson

Il ne peut pas prendre un handle de fichier. L'argument install_requires peut être uniquement une chaîne ou une liste de chaînes .

Vous pouvez bien sûr lire votre fichier dans le script d'installation 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,
...)
84
Fredrick Brennan

Les fichiers de conditions préalables utilisent un format pip développé, ce qui n’est utile que si vous devez compléter votre setup.py par des contraintes plus strictes, par exemple, en spécifiant les URL exactes à partir desquelles certaines dépendances doivent provenir, ou la sortie de pip freeze geler l’ensemble du paquet dans des versions fonctionnant de manière connue. Si vous n'avez pas besoin des contraintes supplémentaires, utilisez uniquement un setup.py. Si vous sentez que vous avez vraiment besoin d'expédier un requirements.txt de toute façon, vous pouvez en faire une seule ligne:

.

Il sera valide et fera exactement référence au contenu du setup.py qui se trouve dans le même répertoire.

61
Tobu

Bien que ce ne soit pas une réponse exacte à la question, je vous recommande d’afficher le blog de Donald Stufft à l’adresse https://caremad.io/2013/07/setup-vs-requirement/ pour une bonne prise en main de ce problème. Je l'utilise avec grand succès.

En bref, requirements.txt n'est pas une alternative setup.py, mais un complément de déploiement. Conservez une abstraction appropriée des dépendances de paquets dans setup.py. Définissez requirements.txt ou plusieurs d'entre eux pour extraire des versions spécifiques de dépendances de packages à des fins de développement, de test ou de production.

Par exemple. avec les paquets inclus dans le repo sous deps/:

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

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

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

37
famousgarkin

La plupart des autres réponses ci-dessus ne fonctionnent pas avec la version actuelle de l'API de pip. Voici la bonne façon * de le faire avec la version actuelle de pip (6.0.8 au moment de l'écriture, aussi travaillé dans 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, c'est le moyen d'utiliser parse_requirements avec le pip actuel. Ce n'est probablement pas la meilleure façon de le faire, car, comme les affiches ci-dessus l'ont dit, pip ne maintient pas vraiment une API.

19
fabianvf

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

Un moyen plus fiable d'éliminer les doubles emplois entre setup.py et requirements.txt consiste à spécifier vos dépendances dans setup.py, puis à placer -e . dans votre fichier requirements.txt. Certaines informations fournies par l’un des pip développeurs et expliquant pourquoi il s’agit d’une meilleure solution sont disponibles ici: https://caremad.io/blog/setup-vs-requirement/

18

Installez le paquet actuel dans Travis. Cela évite l'utilisation d'un fichier requirements.txt. Par exemple:

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

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'))
)
5
reubano

from pip.req import parse_requirements n'a pas fonctionné pour moi et je pense que c'est pour les lignes vides dans les exigences.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
Diego Navarro

ATTENTION AU parse_requirements COMPORTEMENT!

Veuillez noter que pip.req.parse_requirements changera les traits de soulignement en tirets. Cela me mettait en colère quelques jours avant que je ne le découvre. 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']
3
MikeTwo

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

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

Alors je me suis tourné simplement vers l'analyse de texte simple:

with open('requirements.txt', 'r') as f:
    install_reqs = [
        s for s in [
            line.strip(' \n') for line in f
        ] if not s.startswith('#') and s != ''
    ]
3
Dmitriy Sintsov

J'ai créé une fonction réutilisable pour cela. En fait, il analyse un répertoire complet de fichiers de conditions requises et les définit sur extras_require.

Dernière toujours disponible ici: https://Gist.github.com/akatrevorjay/293c26fefa24a7b812f5

import glob
import itertools
import os

from setuptools import find_packages, setup

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:

    >>> _conf = dict(
    ...     name='mainline',
    ...     version='0.0.1',
    ...     description='Mainline',
    ...     author='Trevor Joynson <[email protected],io>',
    ...     url='https://trevor.joynson.io',
    ...     namespace_packages=['mainline'],
    ...     packages=find_packages(),
    ...     Zip_safe=False,
    ...     include_package_data=True,
    ... )
    >>> _conf.update(setup_requirements())
    >>> 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
2
trevorj

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,
    ...
)
1
Brian Bruggeman

Cette approche simple lit le fichier de configuration à partir de setup.py. C'est une variation de la réponse par Dmitiry S. . Cette réponse est compatible uniquement avec Python 3.6+.

Per D.S. , requirements.txt peut documenter des exigences concrètes avec des numéros de version spécifiques, alors que setup.py peut documenter des exigences abstraites avec des plages de versions lâches.

Ci-dessous, 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=Path(__file__).with_name(filename)).readlines()

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

Notez que distutils.text_file.TextFile supprimera les commentaires. De plus, selon mon expérience, vous n'avez apparemment besoin de rien faire pour intégrer le fichier de configuration.

1
Acumenus

Postez ma réponse de this SO question pour une autre solution simple, preuve de 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 __== '__main__':
    setup(
        ...
        install_requires=[str(requirement.req) for requirement in requriements],
        ...
    )

Ajoutez simplement toutes vos exigences sous requirements.txt sous le répertoire racine du projet.

0
Scrotch