Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pyproject.toml: support PEP621 projects too #568

Merged
merged 7 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions lib/bibliothecary/parsers/pypi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class Pypi
MANIFEST_REGEXP = /.*require[^\/]*(\/)?[^\/]*\.(txt|pip|in)$/
PIP_COMPILE_REGEXP = /.*require.*$/

# Adapted from https://peps.python.org/pep-0508/#names
PEP_508_NAME_REGEX = /^([A-Z0-9][A-Z0-9._-]*[A-Z0-9]|[A-Z0-9])/i
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def self.mapping
{
match_filenames('requirements-dev.txt', 'requirements/dev.txt',
Expand Down Expand Up @@ -56,7 +59,7 @@ def self.mapping
},
match_filename("pyproject.toml") => {
kind: 'manifest',
parser: :parse_poetry
parser: :parse_pyproject
},
match_filename("poetry.lock") => {
kind: 'lockfile',
Expand Down Expand Up @@ -90,9 +93,28 @@ def self.parse_pipfile(file_contents, options: {})
map_dependencies(manifest['packages'], 'runtime') + map_dependencies(manifest['dev-packages'], 'develop')
end

def self.parse_pyproject(file_contents, options: {})
deps = []

file_contents = Tomlrb.parse(file_contents)

# Parse poetry [tool.poetry] deps
poetry_manifest = file_contents.fetch('tool', {}).fetch('poetry', {})
deps += map_dependencies(poetry_manifest['dependencies'], 'runtime')
deps += map_dependencies(poetry_manifest['dev-dependencies'], 'develop')

# Parse PEP621 [project] deps
pep621_manifest = file_contents.fetch('project', {})
pep621_deps = pep621_manifest.fetch('dependencies', []).map { |d| parse_pep_508_dep_spec(d) }
deps += map_dependencies(pep621_deps, 'runtime')
Comment on lines +101 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should these be mutually excluded or is it possible for a single manifest to specify both of these dependencies?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP621 is the "tool-agnostic" way to declare build system requirements in pyproject.toml, and from what I understand Poetry is the only build system that doesn't support it yet. They're discussing it tho, and it looks like one of the examples includes both the "tool.poetry" And "project" namespaces for declaring deps: python-poetry/roadmap#3

Copy link
Contributor Author

@tiegz tiegz Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yeah, just found an example here. The deps are the same in both sections, but still seems safer to combine them both until we have a reason not to: https://sourcegraph.com/github.com/IceTiki/ruoli-sign-optimization/-/blob/pyproject.toml?subtree=true

Copy link
Contributor

@mpace965 mpace965 Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, does deps need to be deduped? I'm not sure what clients of this method are expecting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes! Good call, I think we do that higher up in Bibliothecary.analyse_file but it does seem like some of the platform-specific parsers will uniq too.


deps
end

# TODO: this was deprecated in 8.5.2. Remove this in any major version bump >= 9.*
mpace965 marked this conversation as resolved.
Show resolved Hide resolved
def self.parse_poetry(file_contents, options: {})
manifest = Tomlrb.parse(file_contents).fetch('tool', {}).fetch('poetry', {})
map_dependencies(manifest['dependencies'], 'runtime') + map_dependencies(manifest['dev-dependencies'], 'develop')
puts "Warning: parse_poetry() is deprecated, use parse_pyproject() instead."
parse_pyproject(file_contents, options)
end

def self.parse_conda(file_contents, options: {})
Expand Down Expand Up @@ -252,6 +274,15 @@ def self.pip_compile?(file_contents)
# parsing after we match.
false
end

# Simply parses out the name of a PEP 508 Dependency specification: https://peps.python.org/pep-0508/
# Leaves the rest as-is with any leading semicolons or spaces stripped
def self.parse_pep_508_dep_spec(dep)
name, requirement = dep.split(PEP_508_NAME_REGEX, 2).last(2).map(&:strip)
requirement = requirement.sub(/^[\s;]*/, "")
requirement = "*" if requirement == ""
return name, requirement
end
end
end
end
2 changes: 1 addition & 1 deletion lib/bibliothecary/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Bibliothecary
VERSION = "8.5.1"
VERSION = "8.6.0"
end
32 changes: 32 additions & 0 deletions spec/parsers/pypi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,38 @@
})
end

# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#declaring-project-metadata
it 'handles pyproject.toml with pep621-style deps' do
source = <<~FILE
[project]
name = "a_pep621_project"
version = "0.1.0"
dependencies = [
"black",
"isort",
"pytest == 7.2.1",
"python-gitlab == 3.12.0",
"Click~=8.1.0",
"marshmallow-dataclass[union]~=8.5.6",
]
FILE

expect(described_class.analyse_contents('pyproject.toml', source)).to eq({
platform: "pypi",
path: "pyproject.toml",
dependencies: [
{name: "black", requirement: "*", type: "runtime"},
{name: "isort", requirement: "*", type: "runtime"},
{name: "pytest", requirement: "== 7.2.1", type: "runtime"},
{name: "python-gitlab", requirement: "== 3.12.0", type: "runtime"},
{name: "Click", requirement: "~=8.1.0", type: "runtime"},
{name: "marshmallow-dataclass", requirement: "[union]~=8.5.6", type: "runtime"}
],
kind: 'manifest',
success: true
})
end

it 'parses dependencies from Poetry.lock' do
expect(described_class.analyse_contents('poetry.lock', load_fixture('poetry.lock'))).to eq({
platform: "pypi",
Expand Down