Ich habe eine requirements.txt
- Datei, die ich mit Travis-CI verwende. Es erscheint unsinnig, die Anforderungen sowohl in requirements.txt
Als auch in setup.py
Zu duplizieren, und ich hatte gehofft, ein Datei-Handle an das install_requires
- Kwarg in setuptools.setup
Zu übergeben.
Ist das möglich? Wenn ja, wie soll ich vorgehen?
Hier ist meine requirements.txt
Datei:
guessit>=0.5.2
tvdb_api>=1.8.2
hachoir-metadata>=1.3.3
hachoir-core>=1.3.3
hachoir-parser>=1.3.4
Sie können es umdrehen und die Abhängigkeiten in setup.py
Auflisten und stattdessen ein einzelnes Zeichen - einen Punkt .
- in requirements.txt
Eingeben.
Alternativ kann die Datei requirements.txt
(Falls sie keine externen Anforderungen per URL enthält) mit folgendem Hack (getestet mit pip 9.0.1
) Analysiert werden, auch wenn dies nicht empfohlen wird:
install_reqs = parse_requirements('requirements.txt', session='hack')
Dies filtert jedoch nicht mgebungsmarkierungen .
In alten Versionen von pip, genauer gesagt älter als 6. , gibt es eine öffentliche API, mit der dies erreicht werden kann. Eine Anforderungsdatei kann Kommentare (#
) Und einige andere Dateien (--requirement
Oder -r
) Enthalten. Wenn Sie also wirklich einen requirements.txt
Analysieren möchten, können Sie den Pip-Parser verwenden:
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
)
Auf den ersten Blick scheint es, dass requirements.txt
und setup.py
sind alberne Duplikate, aber es ist wichtig zu verstehen, dass die beabsichtigte Funktion sehr unterschiedlich ist, obwohl die Form ähnlich ist.
Das Ziel eines Paketautors ist es, bei der Angabe von Abhängigkeiten zu sagen: "Wo immer Sie dieses Paket installieren, sind dies die anderen Pakete, die Sie benötigen, damit dieses Paket funktioniert."
Im Gegensatz dazu hat der Bereitstellungsautor (der möglicherweise dieselbe Person zu einem anderen Zeitpunkt ist) einen anderen Job: "Hier ist die Liste der Pakete, die wir zusammengestellt und getestet haben und die ich jetzt installieren muss".
Der Autor des Pakets schreibt für eine Vielzahl von Szenarien, da sie ihre Arbeit so einsetzen, dass sie sie möglicherweise nicht kennen und nicht wissen, welche Pakete neben ihrem Paket installiert werden. Um ein guter Nachbar zu sein und Konflikte zwischen Abhängigkeitsversionen und anderen Paketen zu vermeiden, müssen sie einen möglichst großen Bereich von Abhängigkeitsversionen angeben, die möglicherweise funktionieren. Das ist was install_requires
im setup.py
tut.
Der Bereitstellungsautor schreibt für ein ganz anderes, ganz bestimmtes Ziel: eine einzelne Instanz einer installierten Anwendung oder eines installierten Dienstes auf einem bestimmten Computer. Um eine Bereitstellung genau zu steuern und sicherzustellen, dass die richtigen Pakete getestet und bereitgestellt werden, muss der Bereitstellungsautor die genaue Version und den Quellspeicherort jedes zu installierenden Pakets angeben, einschließlich der Abhängigkeiten und Abhängigkeiten der Abhängigkeiten. Mit dieser Spezifikation kann eine Bereitstellung wiederholt auf mehrere Computer angewendet oder auf einem Testcomputer getestet werden, und der Bereitstellungsautor kann sich darauf verlassen, dass jedes Mal dieselben Pakete bereitgestellt werden. Das ist was für ein requirements.txt
tut.
Sie sehen also, dass beide Pakete und Versionen zwar eine große Liste von Paketen und Versionen darstellen, diese beiden Dinge jedoch sehr unterschiedliche Aufgaben haben. Und es ist definitiv einfach, dies zu verwechseln und falsch zu verstehen! Aber der richtige Weg, darüber nachzudenken, ist, dass requirements.txt
ist eine "Antwort" auf die "Frage", die die Anforderungen in allen verschiedenen setup.py
Paketdateien. Anstatt es von Hand zu schreiben, wird es oft erzeugt, indem Pip angewiesen wird, sich alle setup.py
-Dateien in einer Reihe gewünschter Pakete finden eine Reihe von Paketen, von denen angenommen wird, dass sie alle Anforderungen erfüllen, und dann, nachdem sie installiert wurden, diese Liste von Paketen in einer Textdatei "einfrieren" (hier wird pip freeze
Name kommt von).
Also das Mitnehmen:
setup.py
sollte die lockersten möglichen Abhängigkeitsversionen deklarieren, die noch funktionieren. Ihre Aufgabe ist es zu sagen, womit ein bestimmtes Paket arbeiten kann.requirements.txt
ist ein Bereitstellungsmanifest, das einen gesamten Installationsjob definiert und nicht an ein einzelnes Paket gebunden sein sollte. Seine Aufgabe ist es, eine vollständige Liste aller erforderlichen Pakete zu deklarieren, damit eine Bereitstellung funktioniert.Es kann keine Dateizugriffsnummer annehmen. Das Argument install_requires
Kann nur eine Zeichenfolge oder eine Liste von Zeichenfolgen sein.
Sie können Ihre Datei natürlich im Setup-Skript lesen und als Liste von Zeichenfolgen an install_requires
Übergeben.
import os
from setuptools import setup
with open('requirements.txt') as f:
required = f.read().splitlines()
setup(...
install_requires=required,
...)
Anforderungsdateien verwenden ein erweitertes Pip-Format. Dies ist nur nützlich, wenn Sie Ihren setup.py
Durch stärkere Einschränkungen ergänzen müssen, z. B. die genauen URLs angeben, von denen einige Abhängigkeiten stammen müssen, oder die Ausgabe von pip freeze
, um den gesamten Paketsatz auf bekannte Arbeitsversionen einzufrieren. Wenn Sie die zusätzlichen Einschränkungen nicht benötigen, verwenden Sie nur einen setup.py
. Wenn Sie das Gefühl haben, dass Sie wirklich einen requirements.txt
Versenden müssen, können Sie eine einzelne Zeile erstellen:
.
Es ist gültig und bezieht sich genau auf den Inhalt von setup.py
, Der sich im selben Verzeichnis befindet.
Obwohl ich keine genaue Antwort auf die Frage habe, empfehle ich Donald Stuffts Blog-Post unter https://caremad.io/2013/07/setup-vs-requirement/ , um dieses Problem gut zu lösen. Ich habe es mit großem Erfolg eingesetzt.
Kurz gesagt, requirements.txt
Ist keine setup.py
- Alternative, sondern eine Bereitstellungsergänzung. Behalten Sie eine angemessene Abstraktion der Paketabhängigkeiten in setup.py
. Setzen Sie requirements.txt
Oder mehr von ihnen, um bestimmte Versionen von Paketabhängigkeiten für Entwicklung, Test oder Produktion abzurufen.
Z.B. mit im Repo enthaltenen Paketen unter deps/
:
# fetch specific dependencies
--no-index
--find-links deps/
# install package
# NOTE: -e . for editable mode
.
pip führt das Paket setup.py
aus und installiert die spezifischen Versionen der in install_requires
deklarierten Abhängigkeiten. Es gibt keine Duplizität und der Zweck beider Artefakte bleibt erhalten.
Die meisten anderen Antworten funktionieren mit der aktuellen Version der API von pip nicht. Hier ist der richtige * Weg, dies mit der aktuellen Version von pip zu tun (6.0.8 zum Zeitpunkt des Schreibens, funktionierte auch in 7.1.2. Sie können Ihre Version mit pip -V überprüfen).
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
....
)
* Richtig, da es die Art ist, parse_requirements mit dem aktuellen pip zu verwenden. Es ist wahrscheinlich immer noch nicht der beste Weg, dies zu tun, da pip, wie oben bereits erwähnt, keine API unterhält.
Die Verwendung von parse_requirements
Ist problematisch, da die Pip-API nicht öffentlich dokumentiert und unterstützt wird. In Pip 1.6 ist diese Funktion tatsächlich in Bewegung, sodass vorhandene Verwendungen wahrscheinlich nicht mehr funktionieren.
Eine zuverlässigere Methode, um Doppelungen zwischen setup.py
Und requirements.txt
Zu vermeiden, besteht darin, Ihre Abhängigkeiten in setup.py
Anzugeben und dann -e .
In requirements.txt
Einzufügen. Datei. Einige Informationen von einem der pip
Entwickler darüber, warum dies ein besserer Weg ist, finden Sie hier: https://caremad.io/blog/setup-vs-requirement/
Installieren Sie das aktuelle Paket in Travis. Dies vermeidet die Verwendung eines requirements.txt
Datei. Zum Beispiel:
language: python
python:
- "2.7"
- "2.6"
install:
- pip install -q -e .
script:
- python runtests.py
Wenn Sie Ihre Benutzer nicht zwingen möchten, pip zu installieren, können Sie das Verhalten folgendermaßen emulieren:
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
hat bei mir nicht funktioniert und ich denke, es ist für die Leerzeilen in meiner requirements.txt, aber diese Funktion funktioniert
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,
...
)
HÜTE DICH VOR parse_requirements
VERHALTEN!
Bitte beachte, dass pip.req.parse_requirements
ändert Unterstriche in Bindestriche. Das machte mich ein paar Tage wütend, bevor ich es entdeckte. Beispiel zur Veranschaulichung:
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
produziert
['example-with-underscores', 'example-with-dashes']
Die folgende Schnittstelle ist in Pip 10 veraltet:
from pip.req import parse_requirements
from pip.download import PipSession
Also habe ich es einfach auf die einfache Textanalyse umgestellt:
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 != ''
]
Ich habe dafür eine wiederverwendbare Funktion erstellt. Tatsächlich wird ein ganzes Verzeichnis von Anforderungsdateien analysiert und auf extras_require gesetzt.
Neueste immer verfügbar hier: 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
Eine andere mögliche Lösung ...
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
und dann zu benutzen ...
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,
...
)
Dieser einfache Ansatz liest die Anforderungsdatei aus setup.py
. Es ist eine Variation der Antwort von Dmitiry S. . Diese Antwort ist nur mit Python 3.6+ kompatibel.
Per D.S. , requirements.txt
kann konkrete Anforderungen mit bestimmten Versionsnummern dokumentieren, während setup.py
kann abstrakte Anforderungen mit losen Versionsbereichen dokumentieren.
Unten ist ein Auszug aus meinem 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'),
...)
Beachten Sie, dass distutils.text_file.TextFile
entfernt Kommentare. Meiner Erfahrung nach müssen Sie anscheinend auch keine besonderen Schritte unternehmen, um die Anforderungsdatei zu bündeln.
Cross-Posting meiner Antwort von this SO question für eine andere einfache, Pip-Version Proof-Lösung.
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],
...
)
Dann werfen Sie einfach alle Ihre Anforderungen unter requirements.txt
unter Projektstammverzeichnis.