การอ้างอิงข้อกำหนด.txtสำหรับ install_requires kwarg ในไฟล์ setuptools setup.py


279

ฉันมีrequirements.txtไฟล์ที่ฉันใช้กับ Travis-CI ดูเหมือนว่าโง่ที่จะทำซ้ำข้อกำหนดในทั้งคู่requirements.txtและsetup.pyดังนั้นฉันจึงหวังว่าจะส่งไฟล์จัดการไปยังinstall_requireskwarg setuptools.setupค่ะ

เป็นไปได้ไหม ถ้าเป็นเช่นนั้นฉันควรทำอย่างไร

นี่คือ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

4
install_requiresใช้เพื่อประกาศการขึ้นต่อกันของแพ็กเกจที่จำเป็นสำหรับแพ็กเกจเพื่อทำงานและถูกใช้โดยผู้พัฒนาแพ็กเกจขณะที่requirements.txtใช้เพื่อทำการติดตั้งสภาวะแวดล้อมโดยอัตโนมัติซึ่งอนุญาตให้ติดตั้งซอฟต์แวร์พิเศษและทำการตรึงเวอร์ชันและใช้ sysadmins บรรจุภัณฑ์ บทบาทและกลุ่มเป้าหมายของพวกเขาแตกต่างกันอย่างมากดังนั้นการพยายามรวมพวกเขาเข้าด้วยกันเช่นความปรารถนาของโอพีจึงเป็นความผิดพลาดของการออกแบบที่แท้จริง
Zart

7
2 เซนต์ของฉัน อย่าใช้ requirements.txt ใน setup.py วัตถุประสงค์มีความแตกต่างกันระวังcaremad.io/2013/07/setup-vs-requirement
Philippe Ombredanne

3
ฉันเห็นคำตอบที่ซับซ้อนมากมาย มีอะไรผิดปกติกับเก่า[line.strip() for line in open("requirements.txt").readlines()]?
เฟลิเป้ SS ชไนเดอร์

ไม่แนะนำให้ทำเช่นนี้ แต่ถ้าจำเป็นจริงๆมันก็ตรงไปตรงมา: setuptoolsมีทุกสิ่งที่จำเป็นอยู่แล้วpkg_resources.parse_requirements()
sinoroc

คำตอบ:


246

คุณสามารถพลิกมันไปรอบ ๆ และแสดงรายการการพึ่งพาsetup.pyและมีอักขระตัวเดียว - จุด.- requirements.txtแทน


หรือแม้ว่าจะไม่ได้รับคำแนะนำก็ยังสามารถแยกวิเคราะห์requirements.txtไฟล์ได้ (หากไม่ได้อ้างอิงข้อกำหนดภายนอกด้วย URL) ด้วยแฮ็คต่อไปนี้ (ทดสอบด้วยpip 9.0.1):

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

นี้ไม่ได้กรองเครื่องหมายสภาพแวดล้อมแม้ว่า


ใน pip เวอร์ชันเก่าซึ่งมีอายุมากกว่า 6.0โดยเฉพาะมี API สาธารณะที่สามารถใช้เพื่อให้บรรลุสิ่งนี้ ไฟล์ความต้องการสามารถมีความคิดเห็น ( #) และสามารถรวมไฟล์อื่น ๆ ( --requirementหรือ-r) ดังนั้นหากคุณต้องการแยกวิเคราะห์จริงๆrequirements.txtคุณสามารถใช้ parser 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
เกิดอะไรขึ้นถ้าผู้ใช้ไม่ได้ติดตั้ง pip Ka-บูม?
Gringo Suave

83
@GringoSuave หากผู้ใช้ไม่ได้ติดตั้ง pip เขาต้องติดตั้งก่อน
guettli

7
คุณต้องระบุ URL ในไฟล์ข้อกำหนดของคุณในกรณีที่มีบรรทัด -e หรือ -f ("repot" git repo) ที่ชี้ไปยังแพ็คเกจที่ไม่ใช่ pypi ใช้สิ่งนี้:setup(..., dependency_links=[str(req_line.url) for req_line in parse_requirements(<requirements_path>)], ...)
เตาแก๊ส

91
คุณไม่ต้องการทำสิ่งนี้จริงๆ การพูดเป็น pip pipererer ไม่สนับสนุนการถูกเรียกว่าเป็น API เช่นนี้เลย ในความเป็นจริง pip 1.6 (รุ่นถัดไปในขณะนี้) ย้ายฟังก์ชันนี้
Donald Stufft

26
นี่ไม่ควรเป็นคำตอบที่ยอมรับได้อีกต่อไปหากเคยมี มันหักโจ๋งครึ่ม แม้ว่ามันจะทำงานได้ก็ไม่จำเป็นอย่างโจ๋งครึ่ม ตั้งแต่pipเริ่มต้นที่แยกจากการอ้างอิงsetup.pyในกรณีที่ไม่มีrequirements.txtการตอบง่ายๆอย่างชาญฉลาดที่ระบุไว้โดยTobuด้านล่างคือรายการอ้างอิงทั้งหมดในและลบsetup.py requirements.txtสำหรับแอปพลิเคชั่นที่ต้องการทั้งสองอย่างเพียงแค่ลดรายการการพึ่งพาrequirements.txtให้เหลือเพียง.ตัวละคร เสร็จสิ้น
เซซิลแกงกะหรี่

194

บนใบหน้าของมันก็จะดูเหมือนว่าrequirements.txtและsetup.pyมีการซ้ำกันโง่ แต่มันเป็นสิ่งสำคัญที่จะเข้าใจว่าในขณะที่รูปแบบจะคล้ายกันตั้งใจฟังก์ชั่นที่แตกต่างกันมาก

เป้าหมายของผู้เขียนแพ็คเกจเมื่อระบุการอ้างอิงคือการพูดว่า "ไม่ว่าคุณจะติดตั้งแพคเกจนี้ที่ใดก็ตามเป็นแพ็คเกจอื่นที่คุณต้องการเพื่อให้แพคเกจนี้ใช้งานได้"

ในทางตรงกันข้ามผู้สร้างการปรับใช้ (ซึ่งอาจเป็นบุคคลเดียวกันในเวลาที่แตกต่างกัน) มีงานที่แตกต่างกันโดยที่พวกเขาพูดว่า "นี่คือรายการแพคเกจที่เรารวบรวมและทดสอบแล้วและตอนนี้ฉันต้องติดตั้ง"

ผู้เขียนแพ็คเกจเขียนถึงสถานการณ์ที่หลากหลายเนื่องจากพวกเขานำเอางานของพวกเขาไปใช้ในวิธีที่พวกเขาอาจไม่รู้และไม่มีทางรู้เลยว่าจะติดตั้งแพ็คเกจอะไรข้างแพ็คเกจของพวกเขา เพื่อที่จะเป็นเพื่อนบ้านที่ดีและหลีกเลี่ยงความขัดแย้งของการพึ่งพากับแพ็คเกจอื่น ๆ พวกเขาจำเป็นต้องระบุช่วงของการพึ่งพาที่หลากหลายเท่าที่จะทำได้ นี่คือสิ่งที่install_requiresในsetup.pyไม่

ผู้เขียนการปรับใช้เขียนสำหรับเป้าหมายที่เฉพาะเจาะจงแตกต่างกันมาก: อินสแตนซ์เดียวของแอปพลิเคชันหรือบริการที่ติดตั้งไว้ซึ่งติดตั้งบนคอมพิวเตอร์เฉพาะ เพื่อควบคุมการปรับใช้อย่างแม่นยำและตรวจสอบให้แน่ใจว่ามีการทดสอบและปรับใช้แพคเกจที่ถูกต้องผู้สร้างการปรับใช้จะต้องระบุเวอร์ชันที่แน่นอนและแหล่งที่ตั้งของทุกแพ็คเกจที่จะติดตั้งรวมถึงการอ้างอิงและการพึ่งพา ด้วยข้อมูลจำเพาะนี้การปรับใช้สามารถนำไปใช้ซ้ำกับเครื่องหลายเครื่องหรือทดสอบบนเครื่องทดสอบและผู้เขียนการปรับใช้สามารถมั่นใจได้ว่ามีการใช้งานแพคเกจเดียวกันทุกครั้ง นี่คือสิ่งที่requirements.txtทำ

ดังนั้นคุณจะเห็นได้ว่าในขณะที่ทั้งคู่ดูเหมือนกับรายการแพคเกจและรุ่นใหญ่ทั้งสองสิ่งนี้มีงานที่แตกต่างกันมาก และมันง่ายมากที่จะคลุกเคล้ามันและทำให้มันผิด แต่วิธีที่ถูกต้องในการคิดเกี่ยวกับสิ่งนี้คือนั่นrequirements.txtคือ "คำตอบ" กับ "คำถาม" ที่ถูกกำหนดโดยข้อกำหนดในsetup.pyไฟล์แพ็คเกจต่างๆ แทนที่จะเขียนด้วยมือมันมักจะสร้างโดยบอก pip ให้ดูsetup.pyไฟล์ทั้งหมดในชุดของแพ็คเกจที่ต้องการค้นหาชุดของแพ็คเกจที่คิดว่าเหมาะกับความต้องการทั้งหมดแล้วหลังจากติดตั้งแล้ว "ตรึง "รายการแพ็กเกจนั้นลงในไฟล์ข้อความ (นี่คือที่pip freezeมาของชื่อ)

ดังนั้น Takeaway:

  • setup.pyควรประกาศเวอร์ชันการพึ่งพาที่เป็นไปได้ที่หลวมที่สุดซึ่งยังใช้งานได้ หน้าที่ของมันคือการบอกว่าแพ็คเกจใดที่สามารถใช้งานได้
  • requirements.txtเป็นรายการการนำไปใช้งานที่กำหนดงานติดตั้งทั้งหมดและไม่ควรคำนึงถึงว่าเชื่อมโยงกับแพ็คเกจใดแพ็คเกจหนึ่ง หน้าที่ของมันคือการประกาศรายการที่ครบถ้วนสมบูรณ์ของแพ็คเกจที่จำเป็นทั้งหมดเพื่อให้สามารถใช้งานได้
  • เนื่องจากสองสิ่งนี้มีเนื้อหาและเหตุผลที่แตกต่างกันสำหรับที่มีอยู่จึงไม่สามารถคัดลอกสิ่งหนึ่งไปยังอีกที่หนึ่งได้

อ้างอิง:


10
นี่เป็นหนึ่งในคำอธิบายที่ดีที่สุดที่ให้ฉันสั่งในระเบียบที่เรียกว่าการติดตั้งแพคเกจ! :)
Kounavi

6
มันยังไม่ชัดเจนสำหรับฉันว่าทำไมนักพัฒนาจะควบคุมเวอร์ชันrequirements.txtพร้อมกับแหล่งที่มาของแพคเกจที่มีข้อกำหนดคอนกรีต / แช่แข็งสำหรับการติดตั้งหรือทดสอบ แน่นอนsetup.pyสามารถใช้สำหรับวัตถุประสงค์นี้ภายในโครงการเอง ฉันสามารถจินตนาการได้ว่าการใช้ไฟล์ดังกล่าวเป็นเครื่องมือที่ใช้ในการสนับสนุนการจัดการโครงการ (เช่นการปรับโครงสร้างการสร้างรีลีสเป็นต้น)
Sam Brightman

2
@samBrightman ฉันเห็นด้วยทั้งหมดฉันไม่คิดว่าแพ็กเกจไลบรารีหรือแอปพลิเคชันแพ็คเกจควรส่งไฟล์ requirements.txt ของพวกเขาไปยังที่เก็บพร้อมรหัส ฉันคิดว่าควรเป็นสิ่งประดิษฐ์ที่สร้างขึ้นในระหว่างการทดสอบการสร้างและจากนั้นใช้เพื่อจัดทำเอกสารรายการการสร้างและในที่สุดก็สร้างการปรับใช้สิ่งประดิษฐ์
Jonathan Hanson

6
ดังนั้นคุณกำลังพูดว่าrequirements.txtเป็นเอกสารเพิ่มเติมสำหรับสถานะของโลกที่ผลิตงานสร้างที่กำหนดแม้ว่าจะไม่ได้ใช้ในกระบวนการสร้างตัวเอง? นั่นทำให้รู้สึก แต่ก็ดูเหมือนว่าหลายระบบพึ่งพาการทำสำเนา: เทรวิสติดตั้งบางแพคเกจเริ่มต้น (เดิม) ใน virtualenv requirements.txtของคุณและพูดกับการใช้งาน ถ้าผมถามว่าเพื่อให้แน่ใจว่าการอ้างอิงอยู่ที่ล่าสุดใช้คนยืนยันว่าฉันควรใช้setup.py requirements.txt
Sam Brightman

2
คำแนะนำที่ดีที่สุดที่คุณจะได้รับจากสิ่งนี้คือการหาแบบจำลองที่เหมาะกับคุณจัดทำเอกสารได้ดีและทำให้แน่ใจว่าทุกคนที่คุณทำงานด้วยเข้าใจ ลองคิดดูว่าทำไมคุณถึงทำแต่ละบิตและมันสมเหตุสมผลสำหรับกรณีการใช้งานของคุณหรือไม่ และพยายามอ่านให้ดีเท่าที่จะทำได้เกี่ยวกับสถานะปัจจุบันของการสร้างบรรจุภัณฑ์และการเผยแพร่ใน Python ในกรณีที่สิ่งต่าง ๆ ดีขึ้น แต่อย่ากลั้นหายใจ
Jonathan Hanson

90

ไม่สามารถจัดการไฟล์ได้ install_requiresอาร์กิวเมนต์สามารถเพียง แต่จะเป็นสตริงหรือรายการของสตริง

install_requiresคุณสามารถของหลักสูตรอ่านไฟล์ของคุณในสคริปต์การติดตั้งและผ่านมันเป็นรายการของสตริงไป

import os
from setuptools import setup

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

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

5
แม้ว่าจะมีประโยชน์การเปลี่ยนแปลงข้อกำหนดนี้ของข้อกำหนดจากการประกาศถึงความจำเป็น สิ่งนี้ทำให้เป็นไปไม่ได้ที่เครื่องมือบางอย่างจะค้นหาความต้องการของคุณ ยกตัวอย่างเช่น PyCharm install_requiresมีการติดตั้งอัตโนมัติของความต้องการทั้งหมดที่ระบุไว้ใน อย่างไรก็ตามมันจะไม่ทำงานหากคุณไม่ได้ใช้ไวยากรณ์ที่ประกาศ
Piotr Dobrogost

55
@PiotrDobrogost บางทีผู้พัฒนา PyCharm ควรแก้ไขโปรแกรมของพวกเขาแล้ว setup.pyเป็นโปรแกรมที่ควรรันไม่ใช่ไฟล์ข้อมูลที่ควรแยกวิเคราะห์ นั่นไม่ได้ทำให้คำตอบนี้แย่ลง
Fredrick Brennan

5
ฉันแค่ชี้ปัญหาที่เป็นไปได้ คำตอบนี้ใช้ได้อย่างสมบูรณ์แบบ ไม่ใช่ PyCharm เท่านั้นที่มีปัญหากับข้อมูลที่ถูก "ซ่อน" ไว้เบื้องหลังโค้ด นี่เป็นปัญหาสากลและทำให้มีการย้ายโดยทั่วไปไปยังข้อมูลจำเพาะที่เปิดเผยของข้อมูลเมตาในบรรจุภัณฑ์ Python
Piotr Dobrogost

33
ทำงานได้ดีตราบใดที่คุณใส่include requirements.txtเข้าไปMANIFEST.inไม่เช่นนั้นคุณจะไม่สามารถติดตั้งไลบรารีของคุณจากการกระจายแหล่งที่มา
Pankrat

4
ฉันรู้ว่านี่เป็นคำถามเก่า แต่อย่างน้อยคุณสามารถกำหนดค่า PyCharm เพื่อแยกไฟล์ข้อกำหนดที่ Preferences-> Tools-> เครื่องมือรวม Python -> ไฟล์ข้อกำหนดแพ็คเกจ
lekksi

64

ไฟล์ข้อกำหนดใช้รูปแบบ pip แบบขยายซึ่งมีประโยชน์เฉพาะเมื่อคุณต้องการเสริมsetup.pyข้อ จำกัด ที่แข็งแกร่งเช่นระบุ URL ที่แน่นอนการพึ่งพาบางอย่างต้องมาจากหรือผลลัพธ์ของpip freezeการตรึงแพคเกจทั้งหมดให้เป็นที่รู้จัก รุ่น หากคุณไม่จำเป็นต้อง จำกัด setup.pyพิเศษใช้เพียง หากคุณรู้สึกว่าคุณต้องการส่งจริง ๆrequirements.txtคุณสามารถทำให้เป็นบรรทัดเดียว:

.

มันจะถูกต้องและอ้างอิงถึงเนื้อหาของsetup.pyที่อยู่ในไดเรกทอรีเดียวกัน


9
แต่ในกรณีนี้ก็ลองติดตั้งแอพของฉันด้วย จะทำอย่างไรถ้าฉันไม่ต้องการและต้องการติดตั้ง install_requires เท่านั้น
ffeast

2
หากต้องการอธิบายอย่างละเอียดเกี่ยวกับสิ่งที่ @ffeast ถามว่าหากมีข้อกำหนดอยู่ใน setup.py เท่านั้นจะมีวิธีในการติดตั้งข้อกำหนด (เทียบเท่าpip install -r requirements.txt ) โดยไม่ต้องติดตั้งแพ็คเกจหรือไม่
haridsv

1
@ffeast @haridsv -e .น่าจะเพียงพอแล้ว ตรวจสอบหน้านี้: caremad.io/posts/2013/07/setup-vs-requirement
dexhunter

4
@ DexD ขอให้ยังคงพยายามติดตั้งแอปเอง นี่ไม่ใช่สิ่งที่เราต้องการ
ffeast

38

แม้ว่าจะไม่ใช่คำตอบที่ถูกต้อง แต่ฉันขอแนะนำโพสต์บล็อกของ Donald Stufft ที่https://caremad.io/2013/07/setup-vs-requirement/สำหรับปัญหานี้ ฉันใช้มันเพื่อความสำเร็จที่ยิ่งใหญ่

ในระยะสั้นrequirements.txtไม่ใช่setup.pyทางเลือก แต่เป็นส่วนประกอบในการปรับใช้ setup.pyเก็บเป็นนามธรรมที่เหมาะสมของการอ้างอิงในแพคเกจ ตั้งค่าrequirements.txtหรือมากกว่าของ 'em เพื่อดึงข้อมูลการอ้างอิงแพ็คเกจที่เฉพาะเจาะจงสำหรับการพัฒนาการทดสอบหรือการผลิต

เช่นกับแพ็คเกจที่รวมอยู่ใน repo ภายใต้deps/:

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

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

pip รันแพคเกจและติดตั้งเฉพาะรุ่นการอ้างอิงที่ประกาศในsetup.py install_requiresไม่มีความซ้ำซ้อนและวัตถุประสงค์ของสิ่งประดิษฐ์ทั้งสองนั้นได้รับการเก็บรักษาไว้


7
นี้ไม่ได้ทำงานเมื่อคุณต้องการที่จะให้แพคเกจสำหรับคนอื่น ๆ pip install my-packageในการติดตั้งผ่านทาง หากมีการอ้างอิงสำหรับแพคเกจของฉันไม่ได้อยู่ในของฉันแพคเกจ / setup.py pip install my-packageพวกเขาจะไม่ติดตั้งโดย ฉันไม่สามารถระบุวิธีการจัดทำแพคเกจสำหรับผู้อื่นที่มีการพึ่งพาโดยไม่ต้องระบุอย่างชัดเจนใน setup.py ชอบที่จะรู้ว่าถ้ามีคนได้คิดวิธีการที่จะให้มันแห้งขณะที่ช่วยให้คนอื่น ๆ ในการติดตั้งแพคเกจของฉัน + pip install -r my-package/requirements.txtการอ้างอิงโดยไม่ต้องดาวน์โหลดไฟล์ต้องการและเรียกตนเอง
น่า

2
@Malina requirements.txtแพคเกจที่นี่เป็นอย่างดีโดยไม่ต้องติดตั้ง นั่นคือประเด็นทั้งหมด อัปเดตคำถามเพื่อทำให้สิ่งต่าง ๆ ชัดเจนยิ่งขึ้น อัปเดตลิงก์โพสต์บล็อกที่ล้าสมัยด้วย
famousgarkin

ดังนั้นเมื่อเรียกใช้ setup.py มันจะเรียก requirements.txt สำหรับไฟล์ที่ระบุใน stup.py?
dtracers

นี่เป็นอีกวิธีหนึ่งใน @dtracers requirements.txt ชี้ไปที่แพ็คเกจด้วยตนเองซึ่งสามารถพึ่งพาการอัปเดตของ setup.py ได้ ดังนั้นเมื่อติดตั้งโดยใช้ข้อกำหนดมันทำงานได้และเมื่อติดตั้งผ่าน pip มันก็ทำงานได้เช่นกัน - ในทั้งสองกรณีใช้การพึ่งพาของ setup.py แต่ก็อนุญาตให้ติดตั้งสิ่งต่าง ๆ ได้มากขึ้นเมื่อใช้ requirements.txt
smido

20

การใช้parse_requirementsเป็นปัญหาเนื่องจาก pip API ไม่ได้รับการจัดทำเอกสารและรองรับ ใน pip 1.6 ฟังก์ชั่นนั้นกำลังเคลื่อนไหวดังนั้นการใช้งานที่มีอยู่ของมันจึงมีแนวโน้มที่จะแตก

วิธีที่เชื่อถือได้มากขึ้นในการกำจัดการทำซ้ำระหว่างsetup.pyและrequirements.txtเป็นการระบุการอ้างอิงของคุณsetup.pyจากนั้นใส่-e .ลงในrequirements.txtไฟล์ของคุณ ข้อมูลบางส่วนจากหนึ่งในpipนักพัฒนาเกี่ยวกับสาเหตุที่เป็นวิธีที่ดีกว่าที่จะไปที่นี่: https://caremad.io/blog/setup-vs-requirement/


@Tommy ลองสิ่งนี้: caremad.io/2013/07/setup-vs-requirementนี่เป็นลิงค์เดียวกับที่โพสต์ในคำตอบอื่น
amit

18

คำตอบอื่น ๆ ส่วนใหญ่ไม่ทำงานกับ API ของ pip รุ่นปัจจุบัน นี่คือวิธีที่ถูกต้องในการทำกับเวอร์ชั่นปัจจุบันของ pip (6.0.8 ณ เวลาที่เขียนยังทำงานใน 7.1.2 ด้วยคุณสามารถตรวจสอบเวอร์ชั่นของคุณด้วย 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
    ....
)

* ถูกต้องซึ่งเป็นวิธีการใช้ parse_requirements กับ pip ปัจจุบัน มันอาจจะไม่ใช่วิธีที่ดีที่สุดในการทำเช่นนั้นเนื่องจากผู้โพสต์ข้างต้นบอกว่า pip ไม่ได้ดูแล API จริงๆ


14

ติดตั้งแพ็คเกจปัจจุบันใน Travis เป็นการหลีกเลี่ยงการใช้requirements.txtไฟล์ ตัวอย่างเช่น:

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

2
นี่คือการรวมกันที่ดีที่สุดของ "ถูกต้อง" และ "ปฏิบัติ" ฉันจะเพิ่มว่าถ้าหลังจากการทดสอบผ่านคุณสามารถให้ทราวิสสร้างข้อกำหนด.txtด้วยpip freezeและส่งออกไฟล์นั้นเป็นสิ่งประดิษฐ์ (เช่น S3 หรือบางอย่าง) คุณจะมีวิธีที่ยอดเยี่ยมในการติดตั้งสิ่งที่คุณทำซ้ำ การทดสอบ
Jonathan Hanson

4

from pip.req import parse_requirements ไม่ได้ผลสำหรับฉันและฉันคิดว่าเป็นเพราะบรรทัดว่างใน requirements.txt ของฉัน แต่ฟังก์ชันนี้ใช้งานได้

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

หากคุณไม่ต้องการบังคับให้ผู้ใช้ของคุณติดตั้ง pip คุณสามารถเลียนแบบพฤติกรรมของมันด้วยวิธีนี้:

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

อินเตอร์เฟสต่อไปนี้เลิกใช้งานใน pip 10:

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

ดังนั้นฉันจึงเปลี่ยนเป็นการแยกวิเคราะห์ข้อความแบบง่าย:

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 != ''
    ]

วิธีการง่าย ๆ นี้ใช้งานได้มากกว่า 90% ของเวลา สำหรับผู้ที่ใช้ Python 3.6+ ฉันได้เขียนคำตอบที่pathlibแตกต่างกันไป
คิวเมนตัส

3

setup.pyวิธีนี้ง่ายอ่านแฟ้มความต้องการจาก มันเป็นรูปแบบของคำตอบโดยDmitiry S.เอส คำตอบนี้เข้ากันได้กับ Python 3.6+ เท่านั้น

ต่อDS , requirements.txtสามารถเอกสารความต้องการคอนกรีตที่มีหมายเลขรุ่นที่เฉพาะเจาะจงในขณะที่setup.pyสามารถเอกสารข้อกำหนดนามธรรมที่มีช่วงรุ่นหลวม

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'),
   ...)

โปรดทราบว่าdistutils.text_file.TextFileจะตัดความคิดเห็น นอกจากนี้ตามประสบการณ์ของฉันคุณไม่จำเป็นต้องใช้ขั้นตอนพิเศษในการรวมกลุ่มในไฟล์ข้อกำหนด


2

ระวังparse_requirementsพฤติกรรม!

โปรดทราบว่าpip.req.parse_requirementsจะเปลี่ยนขีดล่างเป็นขีดกลาง นี่ทำให้ฉันโมโหไม่กี่วันก่อนที่ฉันจะค้นพบมัน ตัวอย่างที่แสดงให้เห็น:

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

ผลิต

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

1
ใช้unsafe_nameเพื่อรับเวอร์ชันขีดล่าง:[ir.req.unsafe_name for ir in req_deps if ir.req is not None]
alanjds

5
PIP เป็นแอปพลิเคชั่นไม่ใช่ไลบรารี่ ไม่มี API ที่ได้รับการยอมรับจากสาธารณะและการนำเข้าไปในรหัสของคุณไม่ใช่กรณีการใช้งานที่รองรับ ไม่น่าแปลกใจที่มันมีพฤติกรรมที่ไม่คาดคิด ฟังก์ชั่นภายในของมันไม่เคยตั้งใจจะใช้วิธีนี้
Jonathan Hanson

1

ฉันสร้างฟังก์ชั่นที่ใช้ซ้ำได้สำหรับสิ่งนี้ จริง ๆ แล้วแยกวิเคราะห์ไดเรกทอรีทั้งหมดของไฟล์ข้อกำหนดและตั้งค่าเป็น extras_require

พร้อมใช้งานล่าสุดเสมอที่นี่: 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)

ดีมาก! แม้จะจัดการกับความต้องการแบบเรียกซ้ำด้วย pip ล่าสุด :)
amohr

@amohr ขอบคุณ! ฉันเพิ่งอัปเดตสำหรับ pip ในภายหลังฉันไม่แน่ใจว่าทำไมพวกเขาถึงดำเนินการตามวิธีการโดยย้ายสิ่งต่าง ๆ ไปที่pip._internal... หากคุณไม่มี API ภายนอกที่ใช้งานได้คุณไม่ควรทำลายสิ่งเหล่านั้นทั้งหมด ที่ใช้ทั้งหมดที่คุณให้
trevorj

0

อีกวิธีที่เป็นไปได้ ...

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

แล้วใช้ ...

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,
    ...
)

ที่ไม่treeมาจากไหน?
Francesco Boi

@ FrancescoBoi ถ้าคุณยกโทษให้ฉันเล็กน้อยเพราะไม่ได้นำเสนอวิธีการแก้ปัญหาการทำงานอย่างเต็มที่ ... ต้นไม้เป็นเพียงการสแกนระบบไฟล์ในระบบ (คล้ายกับคำสั่ง "tree" ใน linux) นอกจากนี้โซลูชันของฉันด้านบนอาจไม่ทำงานอย่างสมบูรณ์ในตอนนี้เนื่องจากมีการอัปเดต pip อย่างต่อเนื่องและฉันใช้ pip internals
Brian Bruggeman

0

ฉันจะไม่แนะนำให้ทำสิ่งนี้ ดังกล่าวหลายครั้งinstall_requiresและrequirements.txtแน่นอนไม่ควรจะเป็นรายการเดียวกัน แต่เนื่องจากมีคำตอบที่ทำให้เข้าใจผิดจำนวนมากเกี่ยวกับจุดซ่อนเร้นภายใน API ส่วนตัวดังนั้นจึงควรดูทางเลือกอื่น ๆ ของ saner ...

ไม่จำเป็นต้องใช้pipเพื่อแยกrequirements.txtไฟล์จากสคริปต์setuptools setuptoolsโครงการแล้วมีเครื่องมือทั้งหมดที่จำเป็นในของระดับบนแพคเกจsetup.pypkg_resources

อาจมีลักษณะคล้ายกันมากขึ้นหรือน้อยลง:

#!/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,
)

ในกรณีที่คุณไม่ทราบเหตุผลว่าทำไมหลายคน (รวมตัวเอง) ได้ใช้pip's แยกและไม่ได้pkg_resourcesเป็นมาตั้งแต่ก่อน 2015 มีข้อบกพร่องเช่นgithub.com/pypa/setuptools/issues/470 อันนี้แน่นอนได้รับการแก้ไขในปัจจุบัน แต่ฉันยังคงกลัวที่จะใช้เพราะการใช้งานทั้งสองดูเหมือนจะได้รับการพัฒนาแยกต่างหาก
trevorj

@trevorj ขอบคุณที่ชี้สิ่งนี้มาฉันไม่รู้ ความจริงก็คือทุกวันนี้มันใช้งานได้และการมีส่วนร่วมของ pip ดูเหมือนจะเป็นความคิดที่ไร้สาระสำหรับฉัน (โดยเฉพาะในรูปแบบนี้) ลองดูที่คำตอบอื่น ๆ ดูเหมือนว่าส่วนใหญ่จะมีแนวคิดที่ไม่แนะนำเหมือนกันเล็กน้อยโดยไม่มีการเตือนล่วงหน้า และผู้มาใหม่ก็อาจเป็นไปตามแนวโน้มนี้ หวังว่าการริเริ่มเช่น PEP517 และ PEP518 จะช่วยให้ชุมชนอยู่ห่างจากความบ้าคลั่งนี้
sinoroc

-1

ข้ามการโพสต์คำตอบของฉันจากคำถาม SO นี้สำหรับโซลูชันการพิสูจน์เวอร์ชัน 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],
        ...
    )

จากนั้นเพียงแค่โยนความต้องการทั้งหมดของคุณภายrequirements.txtใต้ไดเรกทอรีรากของโครงการ


-1

ฉันทำอย่างนี้:

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

อีกอย่าง parse_requirementsแฮ็กที่แยกวิเคราะห์เครื่องหมายของสภาพแวดล้อมลง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
)

ควรสนับสนุนทั้ง sdist และไบนารี dists

ตามที่ระบุโดยผู้อื่นparse_requirementsมีข้อบกพร่องหลายประการดังนั้นนี่ไม่ใช่สิ่งที่คุณควรทำในโครงการสาธารณะ แต่อาจพอเพียงสำหรับโครงการภายใน / ส่วนบุคคล


pip 20.1 เปลี่ยน API และไม่สามารถใช้งานเครื่องหมายได้parse_requirements()อีกต่อไปดังนั้นสิ่งนี้จึงล้มเหลว
Tuukka Mustonen

-3

นี่คือแฮ็คที่สมบูรณ์ (ทดสอบด้วยpip 9.0.1) ตามคำตอบของ Romainที่แยกวิเคราะห์requirements.txtและกรองตามตัวทำเครื่องหมายสภาพแวดล้อมปัจจุบัน:

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
นี่เป็นเพียงบางส่วนเท่านั้น หากคุณโทรหาr.match_markers()คุณกำลังประเมินเครื่องหมายซึ่งเป็นสิ่งที่ถูกต้องสำหรับ sdist แต่ถ้าคุณกำลังสร้างอไบนารี (เช่นล้อ) แพคเกจจะมีเพียงรายการห้องสมุดที่จับคู่ของคุณสร้างสภาพแวดล้อมเวลา
Tuukka Mustonen

@TuukkaMustonen ดังนั้นจะหาได้ที่ไหนwheel environment(ถ้าเป็นสิ่งที่คนพยายามจะทำ) เพื่อประเมินเครื่องหมายเทียบกับมัน
Anatoly techtonik

ดูstackoverflow.com/a/41172125/165629bdist_wheelที่ยังควรสนับสนุน extras_requireมันไม่ได้ประเมินเครื่องหมายก็แค่พวกเขาที่จะเพิ่ม
Tuukka Mustonen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.