python - ¿Requerimientos de referencia.txt para el archivo install_requires kwarg en setuptools setup.py?
pip requirements.txt (16)
A primera vista, parece que setup.py
y setup.py
son duplicados estúpidos, pero es importante entender que, si bien la forma es similar, la función deseada es muy diferente.
El objetivo de un autor de paquete, al especificar dependencias, es decir "dondequiera que instale este paquete, estos son los otros paquetes que necesita, para que este paquete funcione".
En contraste, el autor de la implementación (que puede ser la misma persona en un momento diferente) tiene un trabajo diferente, en el sentido de que dice "aquí está la lista de paquetes que hemos reunido y probado y que ahora necesito instalar".
El autor del paquete escribe para una amplia variedad de escenarios, porque están desplegando su trabajo para ser utilizado de una manera que quizás no conozcan, y no tienen forma de saber qué paquetes se instalarán junto con su paquete. Para ser un buen vecino y evitar conflictos de versiones de dependencia con otros paquetes, deben especificar un rango tan amplio de versiones de dependencia como sea posible. Esto es lo que install_requires
en setup.py
hace.
El autor de la implementación escribe para un objetivo muy diferente y muy específico: una instancia única de una aplicación o servicio instalado, instalado en una computadora en particular. Para controlar con precisión una implementación, y asegurarse de que los paquetes correctos se prueban e implementan, el autor de la implementación debe especificar la versión exacta y la ubicación de origen de cada paquete que se instalará, incluidas las dependencias y las dependencias de las dependencias. Con esta especificación, una implementación se puede aplicar repetidamente a varias máquinas, o se puede probar en una máquina de prueba, y el autor de la implementación puede estar seguro de que los mismos paquetes se implementan cada vez. Esto es lo requirements.txt
hace un requirements.txt
.
Así que puedes ver que, si bien ambos parecen una gran lista de paquetes y versiones, estas dos cosas tienen trabajos muy diferentes. ¡Y es definitivamente fácil mezclar esto y hacerlo mal! Pero la manera correcta de pensar esto es que requirements.txt
es una "respuesta" a la "pregunta" planteada por los requisitos en todos los diversos archivos del paquete setup.py
. En lugar de escribirlo a mano, a menudo se genera diciéndole a pip que mire todos los archivos setup.py
en un conjunto de paquetes deseados, encuentre un conjunto de paquetes que crea que se ajusten a todos los requisitos y luego, una vez que estén instalados , "congelar" esa lista de paquetes en un archivo de texto (aquí es de donde viene el nombre de pip freeze
).
Así que la comida para llevar:
-
setup.py
debe declarar las versiones de dependencia mássetup.py
posibles que aún sean viables. Su trabajo es decir con qué puede trabajar un paquete en particular. -
requirements.txt
es un manifiesto de implementación que define un trabajo de instalación completo, y no debe pensarse que esté vinculado a ningún paquete. Su trabajo es declarar una lista exhaustiva de todos los paquetes necesarios para hacer que una implementación funcione. - Debido a que estas dos cosas tienen un contenido y razones tan diferentes para su existencia, no es posible simplemente copiar una en la otra.
Referencias:
- Install_requires vs Requisitos de archivos de la guía del usuario del paquete Python.
Tengo un archivo Requirements.txt que estoy usando con Travis-CI. Parece una tontería duplicar los requisitos tanto requirements.txt
setup.py
como en setup.py
, así que esperaba pasar un identificador de archivo al archivo install_requires
kwarg en setuptools.setup
.
es posible? Si es así, ¿cómo debería hacerlo?
Aquí está mi archivo 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
Aquí hay un hack completo (probado con pip 9.0.1
) basado en la respuesta de Romain que analiza Requirements.txt y lo filtra según los marcadores del entorno actual:
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)
Creé una función reutilizable para esto. En realidad, analiza un directorio completo de archivos de requisitos y los establece en extras_require.
Las últimas siempre disponibles aquí: 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
El uso de parse_requirements
es problemático porque la API pip no se documenta y admite públicamente. En la pip 1.6, esa función realmente se está moviendo, por lo que es probable que se rompan sus usos existentes.
Una forma más confiable de eliminar la duplicación entre setup.py
y setup.py
es especificar sus dependencias en setup.py
y luego poner -e .
en su archivo Requirements.txt. Aquí se encuentra disponible información de uno de los desarrolladores de pip
sobre por qué es una mejor manera de hacerlo: https://caremad.io/blog/setup-vs-requirement/
Instala el paquete actual en Travis. Esto evita el uso de un archivo Requirements.txt. Por ejemplo:
language: python
python:
- "2.7"
- "2.6"
install:
- pip install -q -e .
script:
- python runtests.py
La mayoría de las otras respuestas anteriores no funcionan con la versión actual de la API de pip. Esta es la forma correcta * de hacerlo con la versión actual de pip (6.0.8 en el momento de la redacción, también funcionó en 7.1.2. Puede verificar su versión con 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
....
)
* Correcto, ya que es la forma de usar parse_requirements con el pip actual. Probablemente todavía no sea la mejor manera de hacerlo, ya que, como dicen los carteles anteriores, pip realmente no mantiene una API.
La siguiente interfaz quedó obsoleta en pip 10:
from pip.req import parse_requirements
from pip.download import PipSession
Así que lo cambié al simple análisis de texto:
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 != ''''
]
Los archivos de requisitos utilizan un formato pip ampliado, que solo es útil si necesita complementar su setup.py
con restricciones más estrictas, por ejemplo, especificando las direcciones URL exactas de las cuales deben depender las dependencias, o la salida de la pip freeze
para congelar todo el paquete configurado a las versiones de trabajo conocido. Si no necesita las restricciones adicionales, use solo setup.py
. Si sientes que realmente necesitas enviar un requirements.txt
todos modos, puedes convertirlo en una sola línea:
.
Será válido y se referirá exactamente al contenido del setup.py
que se encuentra en el mismo directorio.
No puede tomar un identificador de archivo. El argumento install_requires
solo puede ser una cadena o una lista de cadenas .
Por supuesto, puede leer su archivo en la secuencia de comandos de configuración y pasarlo como una lista de cadenas para las install_requires
.
import os
from setuptools import setup
with open(''requirements.txt'') as f:
required = f.read().splitlines()
setup(...
install_requires=required,
...)
Otra posible solución ...
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
y luego usar ...
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,
...
)
Otro hackeo de parse_requirements
que también analiza los marcadores de entorno 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
)
Debería soportar tanto distes sdist como binarios.
Como han dicho otros, parse_requirements
tiene varias deficiencias, por lo que esto no es lo que debe hacer en proyectos públicos, pero puede ser suficiente para proyectos internos / personales.
Puede darle la vuelta y listar las dependencias en setup.py
y tener un solo carácter: un punto .
- en requirements.txt
en su lugar.
Alternativamente, incluso si no se recomienda, aún es posible analizar el archivo Requirements.txt (si no remite ningún requisito externo por URL) con el siguiente hack (probado con pip 9.0.1
):
install_reqs = parse_requirements(''requirements.txt'', session=''hack'')
Sin embargo, esto no filtra los marcadores de entorno .
En versiones antiguas de pip, más específicamente que 6.0 , hay una API pública que puede usarse para lograr esto. Un archivo de requisitos puede contener comentarios ( #
) y puede incluir algunos otros archivos ( --requirement
o -r
). Por lo tanto, si realmente desea analizar un Requirements.txt puede usar el analizador 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
)
Si bien no es una respuesta exacta a la pregunta, recomiendo la publicación del blog de Donald Stufft en caremad.io/2013/07/setup-vs-requirement para una buena toma de este problema. Lo he estado utilizando con gran éxito.
En resumen, setup.py
no es una alternativa a setup.py
, sino un complemento de implementación. Mantenga una abstracción apropiada de las dependencias del paquete en setup.py
. Establezca requirements.txt
o más de ellos para obtener versiones específicas de dependencias de paquetes para desarrollo, pruebas o producción.
Por ejemplo, con paquetes incluidos en el repositorio bajo deps/
:
# fetch specific dependencies
--no-index
--find-links deps/
# install package
# NOTE: -e . for editable mode
.
pip ejecuta el setup.py
del paquete e instala las versiones específicas de las dependencias declaradas en install_requires
. No hay duplicidad y se conserva el propósito de ambos artefactos.
Si no quiere forzar a sus usuarios a instalar pip, puede emular su comportamiento con esto:
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
no funcionó para mí y creo que es para las líneas en blanco en mis requisitos.txt, pero esta función funciona
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,
...
)
parse_requirements
CUIDADO con los parse_requirements
!
Tenga en cuenta que pip.req.parse_requirements
cambiará los guiones bajos a guiones. Esto me enfureció unos días antes de que lo descubriera. Ejemplo de demostración:
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
produce
[''example-with-underscores'', ''example-with-dashes'']