Skip to content

Commit 1a7598f

Browse files
committed
Use python to parse python requires syntax and evaluate markers.
Not supported yet: * Custom environment for marker evaluation. * "extras" markers Restore function to `--python-obey-requirements-txt`. This flag will only work on python inputs that are provided as a directory, not a wheel/tar.gz/etc. Tests pass including the obey requirements.txt ones. * Fix bug 'else File.directory?` -> `elsif File.directory?...` lol oops. * Use Dir.entries instead of Dir.glob to allow python package extra syntax, with names like `django[bcrypt]` (Note, extras in requirements aren't evaluated yet)
1 parent 0f4db7d commit 1a7598f

4 files changed

Lines changed: 74 additions & 52 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env python3
2+
3+
#import pkg_resources
4+
import packaging.requirements
5+
import json
6+
import sys
7+
8+
# Expect requirements lines via stdin.
9+
#requirements = pkg_resources.parse_requirements(sys.stdin)
10+
11+
# Process environment markers, if any, and produce a list of requirements for the current environment.
12+
def evaluate_requirements(fd):
13+
all_requirements = [packaging.requirements.Requirement(line) for line in sys.stdin]
14+
15+
for req in all_requirements:
16+
# XXX: Note: marker.evaluate() can be given a dict() containing environment values to overwrite
17+
if req.marker is None or req.marker.evaluate():
18+
if len(req.specifier) > 0:
19+
for spec in req.specifier:
20+
yield "%s%s" % (req.name, spec)
21+
else:
22+
yield str(req.name)
23+
24+
print(json.dumps(list(evaluate_requirements(sys.stdin))))

lib/fpm/package/python.rb

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "rubygems"
66
require "fileutils"
77
require "tmpdir"
8+
require "json"
89

910
# Support for python packages.
1011
#
@@ -324,9 +325,9 @@ def process_maintainer(headers)
324325
# * The path to a python sdist file ending in .tar.gz
325326
# * The path to a python wheel file ending in .whl
326327
def input(package)
327-
if attributes[:python_obey_requirements_txt?]
328-
raise "--python-obey-requirements-txt is temporarily unsupported at this time."
329-
end
328+
#if attributes[:python_obey_requirements_txt?]
329+
#raise "--python-obey-requirements-txt is temporarily unsupported at this time."
330+
#end
330331
explore_environment
331332

332333
path_to_package = download_if_necessary(package, version)
@@ -337,6 +338,10 @@ def input(package)
337338
logger.error("The path doesn't appear to be a python package directory. I expected either a pypackage.toml or setup.py but found neither.", :package => package)
338339
raise "Unable to find python package; tried #{setup_py}"
339340
end
341+
342+
if attributes[:python_obey_requirements_txt?] && File.exist?(File.join(path_to_package, "requirements.txt"))
343+
@requirements_txt = File.read(File.join(path_to_package, "requirements.txt"))
344+
end
340345
end
341346

342347
if File.file?(path_to_package)
@@ -355,7 +360,7 @@ def input(package)
355360
log.error("Failed building python package wheel format. This might be a bug in fpm.")
356361
raise "Failed building python package format."
357362
end
358-
else File.directory?(path_to_package)
363+
elsif File.directory?(path_to_package)
359364
logger.debug("Found directory and assuming it's a python source package.")
360365
safesystem(*attributes[:python_pip], "wheel", "--no-deps", "-w", build_path, path_to_package)
361366

@@ -454,11 +459,12 @@ def download_if_necessary(package, version=nil)
454459

455460
safesystem(*setup_cmd)
456461

457-
files = ::Dir.glob(File.join(target, "*.{whl,tar.gz,zip}"))
462+
#files = ::Dir.glob(File.join(target, "*.{whl,tar.gz,zip}"))
463+
files = ::Dir.entries(target).filter { |entry| entry =~ /\.(whl|tar\.gz|zip)$/ }
458464
if files.length != 1
459465
raise "Unexpected directory layout after `pip download ...`. This might be an fpm bug? The directory contains these files: #{files.inspect}"
460466
end
461-
return files.first
467+
return File.join(target, files.first)
462468
else
463469
# no pip, use easy_install
464470
logger.debug("no pip, defaulting to easy_install", :easy_install => attributes[:python_easyinstall])
@@ -467,7 +473,8 @@ def download_if_necessary(package, version=nil)
467473
"--build-directory", target, want_pkg)
468474
# easy_install will put stuff in @tmpdir/packagename/, so find that:
469475
# @tmpdir/somepackage/setup.py
470-
dirs = ::Dir.glob(File.join(target, "*"))
476+
#dirs = ::Dir.glob(File.join(target, "*"))
477+
files = ::Dir.entries(target).filter { |entry| entry != "." && entry != ".." }
471478
if dirs.length != 1
472479
raise "Unexpected directory layout after easy_install. Maybe file a bug? The directory is #{build_path}"
473480
end
@@ -511,52 +518,46 @@ def load_package_info(path)
511518
self.maintainer = metadata.maintainer
512519

513520
if !attributes[:no_auto_depends?] and attributes[:python_dependencies?]
514-
sys_platform = nil
515-
execmd([attributes[:python_bin], "-c", "import sys; print(sys.platform)"], :stdin => false, :stderr => false) do |stdout|
516-
sys_platform = stdout.read.chomp
517-
end
518-
519-
dep_re = /^([^<>!= ]+)\s*(?:([~<>!=]{1,2})\s*(.*))?$/
520-
521521
# Python Dependency specifiers are a somewhat complex format described here:
522522
# https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers
523523
#
524-
# It would be ideal to support the entire specifier format, but it's unclear if that's necessary
525-
# for most packaging situations.
526-
#
527-
# If a specifier is found to not work under fpm, please file an issue on the fpm issue tracker
528-
# and hopefully support for it can be added.
524+
# We can ask python's packaging module to parse and evaluate these.
525+
# XXX: Allow users to override environnment values.
529526
#
530527
# Example:
531528
# Requires-Dist: tzdata; sys_platform = win32
532529
# Requires-Dist: asgiref>=3.8.1
533530

534-
metadata.requires.each do |text|
535-
dep, environment = text.split(/ *; */)
531+
dep_re = /^([^<>!= ]+)\s*(?:([~<>!=]{1,2})\s*(.*))?$/
532+
533+
reqs = []
534+
535+
# --python-obey-requirements-txt should replace the requirments listed from the metadata
536+
if attributes[:python_obey_requirements_txt?] && !@requirements_txt.nil?
537+
requires = @requirements_txt.split("\n")
538+
else
539+
requires = metadata.requires
540+
end
541+
542+
# Evaluate python package requirements and only show ones matching the current environment
543+
# (Environment markers, etc)
544+
# Additionally, 'extra' features such as a requirement named `django[bcrypt]` isn't quite supported yet,
545+
# since the marker.evaluate() needs to be passed some environment like { "extra": "bcrypt" }
546+
execmd([attributes[:python_bin], File.expand_path(File.join("pyfpm", "parse_requires.py"), File.dirname(__FILE__))]) do |stdin, stdout, stderr|
547+
requires.each { |r| stdin.puts(r) }
548+
stdin.close
549+
data = stdout.read
550+
logger.pipe(stderr => :warn)
551+
reqs += JSON.parse(data)
552+
end
553+
554+
reqs.each do |dep|
536555
match = dep_re.match(dep)
537556
if match.nil?
538557
logger.error("Unable to parse dependency", :dependency => dep)
539558
raise FPM::InvalidPackageConfiguration, "Invalid dependency '#{dep}'"
540559
end
541560

542-
if environment
543-
if environment.include?("sys_platform ==") && !environment.include?("sys_platform == #{sys_platform}")
544-
logger.debug("Ignoring dependency because it doesn't match the current platform", :current => sys_platform, :target => environment, :dependency => text)
545-
next
546-
end
547-
548-
if environment.include?("extra ==")
549-
logger.debug("Ignoring extra/optional dependency", :dependency => text, :extra => environment)
550-
next
551-
end
552-
553-
unsupported_markers = UNSUPPORTED_DEPENDENCY_MARKERS.filter { |m| environment.include?(m) }
554-
if unsupported_markers.any?
555-
logger.debug("Package contains an unsupported Requires-Dist 'environment marker' which fpm doesn't yet support. If you want support for these, please file an issue.", :markers => unsupported_markers, :dependency => text)
556-
next
557-
end
558-
end # if environment
559-
560561
name, cmp, version = match.captures
561562

562563
next if attributes[:python_disable_dependency].include?(name)
@@ -593,7 +594,7 @@ def fix_name(name)
593594
if name.start_with?("python")
594595
# If the python package is called "python-foo" strip the "python-" part while
595596
# prepending the package name prefix.
596-
return [attributes[:python_package_name_prefix], name.gsub(/^python-/, "")].join("-")
597+
return [attributes[:ptython_package_name_prefix], name.gsub(/^python-/, "")].join("-")
597598
else
598599
return [attributes[:python_package_name_prefix], name].join("-")
599600
end

spec/fixtures/python/setup.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@
1010
package_dir={},
1111
install_requires=[
1212
"Dependency1", "dependency2",
13-
# XXX: I don't know what these python_version-dependent deps mean
14-
# needs investigation
15-
# Reference: PEP-0508
1613
'rtxt-dep3; python_version == "2.0"',
1714
'rtxt-dep4; python_version > "2.0"',
1815
],

spec/fpm/package/python_spec.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def easy_install_default(python_bin, option)
149149
# not supported by fpm.
150150
# In this test, there are (at time of writing) some python_version markers and fpm doesn't
151151
# support those.
152-
insist { subject.dependencies.sort } == ["#{prefix}-dependency1","#{prefix}-dependency2"]
152+
insist { subject.dependencies.sort } == ["#{prefix}-dependency1","#{prefix}-dependency2", "#{prefix}-rtxt-dep4"]
153153
end
154154

155155
context "and :python_disable_dependency is set" do
@@ -160,7 +160,7 @@ def easy_install_default(python_bin, option)
160160
it "it should exclude the dependency" do
161161
subject.input(example_dir)
162162
prefix = subject.attributes[:python_package_name_prefix]
163-
insist { subject.dependencies.sort } == ["#{prefix}-dependency2"]
163+
insist { subject.dependencies.sort } == ["#{prefix}-dependency2", "#{prefix}-rtxt-dep4"]
164164
end
165165
end
166166
end
@@ -179,13 +179,14 @@ def easy_install_default(python_bin, option)
179179
it "it should prefix requirements.txt" do
180180
subject.input(example_dir)
181181
prefix = subject.attributes[:python_package_name_prefix]
182-
insist { subject.dependencies.sort } == ["#{prefix}-rtxt-dep3", "#{prefix}-rtxt-dep4"]
182+
insist { subject.dependencies.sort } == ["#{prefix}-rtxt-dep1 > 0.1", "#{prefix}-rtxt-dep2 = 0.1", "#{prefix}-rtxt-dep4"]
183183
end
184184

185185
it "it should exclude the dependency" do
186186
subject.attributes[:python_disable_dependency] = "rtxt-dep1"
187187
subject.input(example_dir)
188-
insist { subject.dependencies.sort } == ["python-rtxt-dep2 = 0.1", "python-rtxt-dep4 "]
188+
prefix = subject.attributes[:python_package_name_prefix]
189+
insist { subject.dependencies.sort } == ["#{prefix}-rtxt-dep2 = 0.1", "#{prefix}-rtxt-dep4"]
189190
end
190191
end
191192

@@ -196,21 +197,20 @@ def easy_install_default(python_bin, option)
196197

197198
it "it should load requirements.txt" do
198199
subject.input(example_dir)
199-
insist { subject.dependencies.sort } == ["rtxt-dep1 > 0.1", "rtxt-dep2 = 0.1", "rtxt-dep4 "]
200+
insist { subject.dependencies.sort } == ["rtxt-dep1 > 0.1", "rtxt-dep2 = 0.1", "rtxt-dep4"]
200201
end
201202

202203
it "it should exclude the dependency" do
203204
subject.attributes[:python_disable_dependency] = "rtxt-dep1"
204205
subject.input(example_dir)
205-
insist { subject.dependencies.sort } == ["rtxt-dep2 = 0.1", "rtxt-dep4 "]
206+
insist { subject.dependencies.sort } == ["rtxt-dep2 = 0.1", "rtxt-dep4"]
206207
end
207208
end
208209
end
209210

210211
context "python_scripts_executable is set" do
211212
it "should have scripts with a custom hashbang line" do
212-
pending("Disabled on travis-ci becaulamese it always fails, and there is no way to debug it?") if is_travis
213-
skip("Requires python3 executable") unless program_exists?("python3")
213+
skip("setup.py-specific feature is no longer supported")
214214

215215
subject.attributes[:python_scripts_executable] = "fancypants"
216216
# Newer versions of Django require Python 3.
@@ -243,7 +243,7 @@ def easy_install_default(python_bin, option)
243243
insist { subject.version } == "8.3.0"
244244
insist { subject.maintainer } == "Pallets <contact@palletsprojects.com>"
245245
insist { subject.architecture } == "all"
246-
insist { subject.depends }.include? "python3-colorama"
246+
insist { subject.dependencies } == [ ]
247247

248248
end
249249
end

0 commit comments

Comments
 (0)