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
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
)
À 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.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,
...)
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.
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é.
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.
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/
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
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'))
)
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,
...
)
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']
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 != ''
]
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
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,
...
)
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.
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.