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

Proposed fix for #474: enables parsing of requirements.txt during locking #476

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 14 additions & 26 deletions conda_lock/src_parser/environment_yaml.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import pathlib
import re
import sys

import tempfile
from typing import List, Tuple

import yaml

from conda_lock.models.lock_spec import Dependency, LockSpecification
from conda_lock.src_parser.conda_common import conda_spec_to_versioned_dep
from conda_lock.src_parser.requirements_txt import parse_requirements_txt, parse_one_requirement
from conda_lock.src_parser.selectors import filter_platform_selectors

from .pyproject_toml import parse_python_requirement


_whitespace = re.compile(r"\s+")
_conda_package_pattern = re.compile(r"^(?P<name>[A-Za-z0-9_-]+)\s?(?P<version>.*)?$")

Expand All @@ -29,6 +26,7 @@ def _parse_environment_file_for_platform(
content: str,
category: str,
platform: str,
source_path: pathlib.Path,
) -> List[Dependency]:
"""
Parse dependencies from a conda environment specification for an
Expand All @@ -55,28 +53,18 @@ def _parse_environment_file_for_platform(

for mapping_spec in mapping_specs:
if "pip" in mapping_spec:
for spec in mapping_spec["pip"]:
if re.match(r"^-e .*$", spec):
print(
(
f"Warning: editable pip dep '{spec}' will not be included in the lock file. "
"You will need to install it separately."
),
file=sys.stderr,
)
continue

dependencies.append(
parse_python_requirement(
spec,
manager="pip",
category=category,
normalize_name=False,
)
)
# Generate the temporary requirements file
yaml_dir = source_path.parent
with tempfile.NamedTemporaryFile(dir=yaml_dir, suffix="requirements.txt", mode='w', encoding='utf-8') as requirements:
requirements.writelines(mapping_spec["pip"])
requirements.write("\n") # Trailing newline
requirements.file.close()

dependencies.extend(parse_requirements_txt(requirements.name, category))
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering what will happen now with editable requirements.


# ensure pip is in target env
dependencies.append(parse_python_requirement("pip", manager="conda"))
if 'pip' not in {d.name for d in dependencies}:
dependencies.append(parse_one_requirement("pip", category))

return dependencies

Expand Down Expand Up @@ -125,7 +113,7 @@ def parse_environment_file(

# Parse with selectors for each target platform
dep_map = {
platform: _parse_environment_file_for_platform(content, category, platform)
platform: _parse_environment_file_for_platform(content, category, platform, environment_file)
for platform in platforms
}

Expand Down
48 changes: 48 additions & 0 deletions conda_lock/src_parser/requirements_txt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Parses pip requirements from a requirements file
"""
from packaging._parser import parse_requirement
from pip._internal.network.session import PipSession
from pip._internal.req.req_file import parse_requirements

from conda_lock.models.lock_spec import VersionedDependency, VCSDependency, URLDependency, Dependency
from conda_lock.src_parser.pyproject_toml import unpack_git_url


def parse_requirements_txt(file_path, category=None) -> Dependency:
Copy link
Contributor

Choose a reason for hiding this comment

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

For the return type annotation, this seems to be Dependency generator rather than returning a Dependency.

session = PipSession()
for req in filter(None, parse_requirements(file_path, session)):
yield parse_one_requirement(req.requirement, category=category)


def parse_one_requirement(req_string: str, category=None) -> Dependency:
parsed_req = parse_requirement(req_string)

if parsed_req.url and parsed_req.url.startswith("git+"):
url, rev = unpack_git_url(parsed_req.url)
return VCSDependency(
name=parsed_req.name,
source=url,
manager='pip',
vcs="git",
rev=rev,
)
elif parsed_req.url: # type: ignore[attr-defined]
assert parsed_req.specifier in {"", "*", None}
url, frag = urldefrag(parsed_req.url) # type: ignore[attr-defined]
return URLDependency(
name=parsed_req.name,
manager='pip',
category=category,
extras=parsed_req.extras,
url=url,
hashes=[frag.replace("=", ":")],
)
else:
return VersionedDependency(
name=parsed_req.name,
version=parsed_req.specifier or "*",
manager='pip',
category=category,
extras=parsed_req.extras,
)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies = [
"pyyaml >= 5.1",
'tomli; python_version<"3.11"',
"typing-extensions",
"pip",
# conda dependencies
"ruamel.yaml",
"toolz >=0.12.0,<1.0.0",
Expand Down