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

Issue 4371 incorrect dependencies when install dev packages #5234

Merged
Show file tree
Hide file tree
Changes from 12 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
20 changes: 17 additions & 3 deletions pipenv/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
is_required_version,
is_star,
pep423_name,
prepare_default_constraint_file,
python_version,
)
from pipenv.utils.indexes import get_source_list, parse_indexes, prepare_pip_source_args
Expand All @@ -41,6 +42,7 @@
cmd_list_to_shell,
find_python,
is_python_command,
normalize_path,
project_python,
subprocess_run,
system_which,
Expand Down Expand Up @@ -788,6 +790,7 @@ def batch_install(
trusted_hosts=trusted_hosts,
extra_indexes=extra_indexes,
use_pep517=use_pep517,
use_constraint=False, # no need to use constraints, it's written in lockfile
)
c.dep = dep

Expand Down Expand Up @@ -1095,8 +1098,9 @@ def do_lock(
for k, v in lockfile[section].copy().items():
if not hasattr(v, "keys"):
del lockfile[section][k]
# Resolve dev-package dependencies followed by packages dependencies.
for is_dev in [True, False]:

# Resolve package to generate constraints before resolving dev-packages
for is_dev in [False, True]:
pipfile_section = "dev-packages" if is_dev else "packages"
if project.pipfile_exists:
packages = project.parsed_pipfile.get(pipfile_section, {})
Expand Down Expand Up @@ -1452,12 +1456,14 @@ def pip_install(
block=True,
index=None,
pre=False,
dev=False,
selective_upgrade=False,
requirements_dir=None,
extra_indexes=None,
pypi_mirror=None,
trusted_hosts=None,
use_pep517=True,
use_constraint=False,
):
piplogger = logging.getLogger("pipenv.patched.pip._internal.commands.install")
if not trusted_hosts:
Expand Down Expand Up @@ -1538,9 +1544,15 @@ def pip_install(
)
pip_command.extend(pip_args)
if r:
pip_command.extend(["-r", vistir.path.normalize_path(r)])
pip_command.extend(["-r", normalize_path(r)])
elif line:
pip_command.extend(line)
if dev and use_constraint:
constraint_filename = prepare_default_constraint_file(
project,
dir=requirements_dir,
)
pip_command.extend(["-c", normalize_path(constraint_filename)])
pip_command.extend(prepare_pip_source_args(sources))
if project.s.is_verbose():
click.echo(f"$ {cmd_list_to_shell(pip_command)}", err=True)
Expand Down Expand Up @@ -2127,9 +2139,11 @@ def do_install(
selective_upgrade=selective_upgrade,
no_deps=False,
pre=pre,
dev=dev,
requirements_dir=requirements_directory,
index=index_url,
pypi_mirror=pypi_mirror,
use_constraint=True,
)
if c.returncode:
sp.write_err(
Expand Down
6 changes: 5 additions & 1 deletion pipenv/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,12 +745,15 @@ def resolve_packages(pre, clear, verbose, system, write, requirements_dir, packa
else None
)

def resolve(packages, pre, project, sources, clear, system, requirements_dir=None):
def resolve(
packages, pre, project, sources, clear, system, dev, requirements_dir=None
):
return resolve_deps(
packages,
which,
project=project,
pre=pre,
dev=dev,
sources=sources,
clear=clear,
allow_global=system,
Expand All @@ -769,6 +772,7 @@ def resolve(packages, pre, project, sources, clear, system, requirements_dir=Non
results, resolver = resolve(
packages,
pre=pre,
dev=dev,
project=project,
sources=sources,
clear=clear,
Expand Down
43 changes: 43 additions & 0 deletions pipenv/utils/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,49 @@ def convert_deps_to_pip(
return f.name


def get_constraints_from_deps(deps):
"""Get contraints from Pipfile-formatted dependency"""
from pipenv.vendor.requirementslib.models.requirements import Requirement

def is_constraint(dep):
# https://pip.pypa.io/en/stable/user_guide/#constraints-files
# constraints must have a name, they cannot be editable, and they cannot specify extras.
return dep.name and not dep.editable and not dep.extras

constraints = []
for dep_name, dep in deps.items():
new_dep = Requirement.from_pipfile(dep_name, dep)
if is_constraint(new_dep):
c = new_dep.as_line().strip()
constraints.append(c)
return constraints


def prepare_default_constraint_file(
project,
dir=None,
Copy link
Member

Choose a reason for hiding this comment

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

We should avoid shadowing built in python function name dir -- I recommend naming it directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fixed it.

):
from pipenv.vendor.vistir.path import (
create_tracked_tempdir,
create_tracked_tempfile,
)

if not dir:
dir = create_tracked_tempdir(suffix="-requirements", prefix="pipenv-")

default_constraints_file = create_tracked_tempfile(
mode="w",
prefix="pipenv-",
suffix="-default-constraints.txt",
dir=dir,
delete=False,
)
default_constraints = get_constraints_from_deps(project.packages)
default_constraints_file.write("\n".join([c for c in default_constraints]))
default_constraints_file.close()
return default_constraints_file.name


def is_required_version(version, specified_version):
"""Check to see if there's a hard requirement for version
number provided in the Pipfile.
Expand Down
59 changes: 54 additions & 5 deletions pipenv/utils/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
from pipenv.patched.pip._internal.operations.build.build_tracker import (
get_build_tracker,
)
from pipenv.patched.pip._internal.req.constructors import (
install_req_from_parsed_requirement,
)
from pipenv.patched.pip._internal.req.req_file import parse_requirements
from pipenv.patched.pip._internal.utils.hashes import FAVORITE_HASH
from pipenv.patched.pip._internal.utils.temp_dir import global_tempdir_manager
Expand All @@ -35,6 +38,7 @@
get_vcs_deps,
is_pinned_requirement,
pep423_name,
prepare_default_constraint_file,
translate_markers,
)
from .indexes import parse_indexes, prepare_pip_source_args
Expand Down Expand Up @@ -117,6 +121,7 @@ def __init__(
skipped=None,
clear=False,
pre=False,
dev=False,
):
self.initial_constraints = constraints
self.req_dir = req_dir
Expand All @@ -126,6 +131,7 @@ def __init__(
self.hashes = {}
self.clear = clear
self.pre = pre
self.dev = dev
self.results = None
self.markers_lookup = markers_lookup if markers_lookup is not None else {}
self.index_lookup = index_lookup if index_lookup is not None else {}
Expand All @@ -134,12 +140,15 @@ def __init__(
self.requires_python_markers = {}
self._pip_args = None
self._constraints = None
self._default_constraints = None
self._parsed_constraints = None
self._parsed_default_constraints = None
self._resolver = None
self._finder = None
self._ignore_compatibility_finder = None
self._session = None
self._constraint_file = None
self._default_constraint_file = None
self._pip_options = None
self._pip_command = None
self._retry_attempts = 0
Expand Down Expand Up @@ -420,6 +429,7 @@ def create(
req_dir: str = None,
clear: bool = False,
pre: bool = False,
dev: bool = False,
) -> "Resolver":

if not req_dir:
Expand Down Expand Up @@ -450,6 +460,7 @@ def create(
skipped=skipped,
clear=clear,
pre=pre,
dev=dev,
)

@classmethod
Expand Down Expand Up @@ -552,6 +563,14 @@ def constraint_file(self):
self._constraint_file = self.prepare_constraint_file()
return self._constraint_file

@property
Copy link
Member

Choose a reason for hiding this comment

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

Can we make this a @cached_property instead so that we don't have to implement that ourselves with if self._default_constraint_file is None: logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used @cached_property but falling some tests on MacOS.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those tests also failed without @cached_property as well, so maybe @cached_property is not the issue. Because @cached_property does not work with python 3.7, I used @property and @lru_cache instead.

Copy link
Member

Choose a reason for hiding this comment

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

I think the problem is you need to import @cached_property from the vendor'd dependencies, go with from pipenv.vendor.cached_property import cached_property until #5169 is completed.

Copy link
Member

Choose a reason for hiding this comment

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

The only tests that are expected could fail, and likely on mac OS right now is test_convert_deps_to_pip and test_convert_deps_to_pip_one_way. Still investigating how these failures started in the main branch, so if you see that specifically its safe to ignore. The other tests should all pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ran with the main branch, but there are more failed tests on macOS other than test_convert_deps_to_pip and test_convert_deps_to_pip_one_way.

Copy link
Member

Choose a reason for hiding this comment

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

@dqkqd It is possible that the new version of setuptools that was released yesterday has affected things. I will re-run the main branch actions, but you can see the last time it ran upon merge ony those *_depts_to_pip tests had failed: https://github.com/pypa/pipenv/runs/7758310446?check_suite_focus=true

Copy link
Member

@matteius matteius Aug 13, 2022

Choose a reason for hiding this comment

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

@dqkqd I fixed the unexpected test failures upstream in main branch, in this PR: #5241

I am still not sure why those *_depts_to_pip tests have sporadic failures in the CI and Mac OS, but thats another can of worms.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matteius I updated as you said. Not sure why it passed *_depts_to_pip cases. I didn't even modify convert_deps_to_pip.

Copy link
Member

Choose a reason for hiding this comment

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

@dqkqd It is a "flakey" test case, maybe I'll mark it as such and it won't fail the build as often. I haven't gotten to the bottom of why sometimes it behaves differently, it has failed on Mac OS most frequently, but I have seen it once in a while fail on other systems.

def default_constraint_file(self):
if self._default_constraint_file is None:
self._default_constraint_file = prepare_default_constraint_file(
self.project, dir=self.req_dir
)
return self._default_constraint_file

@property
def pip_options(self):
if self._pip_options is None:
Expand Down Expand Up @@ -631,11 +650,34 @@ def parsed_constraints(self):
return self._parsed_constraints

@property
Copy link
Member

Choose a reason for hiding this comment

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

Recommend also making this a @cached_property

def constraints(self):
from pipenv.patched.pip._internal.req.constructors import (
install_req_from_parsed_requirement,
)
def parsed_default_constraints(self):
pip_options = self.pip_options
pip_options.extra_index_urls = []
if self._parsed_default_constraints is None:
self._parsed_default_constraints = parse_requirements(
self.default_constraint_file,
constraint=True,
finder=self.finder,
session=self.session,
options=pip_options,
)
return self._parsed_default_constraints

@property
Copy link
Member

Choose a reason for hiding this comment

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

And this one also I think would be better as a cached_property.

def default_constraints(self):
if self._default_constraints is None:
self._default_constraints = [
install_req_from_parsed_requirement(
c,
isolated=self.pip_options.build_isolation,
user_supplied=False,
)
for c in self.parsed_default_constraints
]
return self._default_constraints

@property
def constraints(self):
if self._constraints is None:
self._constraints = [
install_req_from_parsed_requirement(
Expand All @@ -646,6 +688,9 @@ def constraints(self):
)
for c in self.parsed_constraints
]
# Only use default_constraints when installing dev-packages
if self.dev:
Copy link
Member

Choose a reason for hiding this comment

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

Could you explain more why we only include the default_constraints when dev is specified? I don't think I quite understand out of the gate. I got to looking at this more because this is the only place that uses self.dev, and I am noticing how many parameters pip_install takes already.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think constraint is used only when installing dev-packages. If it does with new normal packages, the installing package could not overwrite existing packages, because it use them as contraint, and therefore might not be installed.

Copy link
Member

Choose a reason for hiding this comment

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

Ah ok thanks, that makes sense.

self._constraints += self.default_constraints
return self._constraints

@contextlib.contextmanager
Expand Down Expand Up @@ -870,6 +915,7 @@ def actually_resolve_deps(
sources,
clear,
pre,
dev,
req_dir=None,
):
if not req_dir:
Expand All @@ -878,7 +924,7 @@ def actually_resolve_deps(

with warnings.catch_warnings(record=True) as warning_list:
resolver = Resolver.create(
deps, project, index_lookup, markers_lookup, sources, req_dir, clear, pre
deps, project, index_lookup, markers_lookup, sources, req_dir, clear, pre, dev
)
resolver.resolve()
hashes = resolver.resolve_hashes()
Expand Down Expand Up @@ -1064,6 +1110,7 @@ def resolve_deps(
python=False,
clear=False,
pre=False,
dev=False,
allow_global=False,
req_dir=None,
):
Expand Down Expand Up @@ -1094,6 +1141,7 @@ def resolve_deps(
sources,
clear,
pre,
dev,
req_dir=req_dir,
)
except RuntimeError:
Expand Down Expand Up @@ -1122,6 +1170,7 @@ def resolve_deps(
sources,
clear,
pre,
dev,
req_dir=req_dir,
)
except RuntimeError:
Expand Down
26 changes: 26 additions & 0 deletions tests/integration/test_install_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,3 +509,29 @@ def test_install_with_unnamed_source(PipenvInstance):
f.write(contents)
c = p.pipenv("install")
assert c.returncode == 0

@pytest.mark.dev
@pytest.mark.install
def test_install_dev_use_default_constraints(PipenvInstance):
# See https://github.com/pypa/pipenv/issues/4371
# See https://github.com/pypa/pipenv/issues/2987
with PipenvInstance(chdir=True) as p:

c = p.pipenv("install requests==2.14.0")
assert c.returncode == 0
assert "requests" in p.lockfile["default"]
assert p.lockfile["default"]["requests"]["version"] == "==2.14.0"

c = p.pipenv("install --dev requests")
assert c.returncode == 0
assert "requests" in p.lockfile["develop"]
assert p.lockfile["develop"]["requests"]["version"] == "==2.14.0"

# requests 2.14.0 doesn't require these packages
assert "idna" not in p.lockfile["develop"]
assert "certifi" not in p.lockfile["develop"]
assert "urllib3" not in p.lockfile["develop"]
assert "chardet" not in p.lockfile["develop"]

c = p.pipenv("run python -c 'import urllib3'")
assert c.returncode != 0
35 changes: 35 additions & 0 deletions tests/integration/test_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,3 +791,38 @@ def test_pipenv_respects_package_index_restrictions(PipenvInstance):
'sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a'],
'index': 'local', 'version': '==2.19.1'}
assert p.lockfile['default']['requests'] == expected_result


@pytest.mark.dev
@pytest.mark.lock
@pytest.mark.install
def test_dev_lock_use_default_packages_as_constraint(PipenvInstance):
Copy link
Member

Choose a reason for hiding this comment

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

When I run this test in the debugger, it gets to the method pip_install but use_constraint=False and so it does not exercise the method write_constraint_to_file as I would have thought. Going to try with your other tests to see.

Copy link
Contributor Author

@dqkqd dqkqd Aug 9, 2022

Choose a reason for hiding this comment

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

Actually there are 2 constraints files. The first one is inside resolver.py

@property
def default_constraint_file(self):
if self._default_constraint_file is None:
self._default_constraint_file = self.prepare_default_constraint_file()
return self._default_constraint_file

The second one is write_constraint_to_file as mention above.

  • The first constraints file is created and used when: Locking or Installing with Pipfile using --dev flag (user don't pass any packages args).
  • The second constrains file is created and used when: User pass packages as argument using --dev flag.

When locking, the code does the following things:

  • Resolve dependencies with venv_resolve_deps (this create and use the first constraints file).
  • Running batch_install, install all packages in the lock file. This one calls pip_install, this could create the second constraints file (if both dev and use_constraint equal True). But since all the packages were resolved in step 1 and written to lockfile, so resolving isn't necessary.

    pipenv/pipenv/core.py

    Lines 785 to 793 in 30a6b1a

    no_deps=skip_dependencies,
    block=is_blocking,
    index=dep.index,
    requirements_dir=requirements_dir,
    pypi_mirror=pypi_mirror,
    trusted_hosts=trusted_hosts,
    extra_indexes=extra_indexes,
    use_pep517=use_pep517,
    use_constraint=False, # no need to use constraints, it's written in lockfile

Copy link
Member

Choose a reason for hiding this comment

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

Thanks @dqkqd -- I was able to hit that breakpoint in the test test_install_dev_use_default_constraints after I posted the first message. Can you explain more why to have two different methods of similar code for generating the two different constraints files?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have lock and install method. The first constraint file in resolver.py is used for lock, the second one in core.py is used for install.

  1. The first one should not be removed, because it would cause lockfile is not generated correctly.
  2. The second one could be remove. Actually, this is my first approach.
    Suppose I remove this one. Then pip_install inside this loop should not be called, because it will install packages without resolving dependencies.

    pipenv/pipenv/core.py

    Lines 2136 to 2271 in 30a6b1a

    for pkg_line in pkg_list:
    click.secho(
    fix_utf8(f"Installing {pkg_line}..."),
    fg="green",
    bold=True,
    )
    # pip install:
    with vistir.contextmanagers.temp_environ(), create_spinner(
    "Installing...", project.s
    ) as sp:
    if not system:
    os.environ["PIP_USER"] = "0"
    if "PYTHONHOME" in os.environ:
    del os.environ["PYTHONHOME"]
    sp.text = f"Resolving {pkg_line}..."
    try:
    pkg_requirement = Requirement.from_line(pkg_line)
    except ValueError as e:
    sp.write_err("{}: {}".format(click.style("WARNING", fg="red"), e))
    sp.red.fail(
    environments.PIPENV_SPINNER_FAIL_TEXT.format(
    "Installation Failed"
    )
    )
    sys.exit(1)
    sp.text = "Installing..."
    try:
    sp.text = f"Installing {pkg_requirement.name}..."
    if project.s.is_verbose():
    sp.hide_and_write(
    f"Installing package: {pkg_requirement.as_line(include_hashes=False)}"
    )
    c = pip_install(
    project,
    pkg_requirement,
    ignore_hashes=True,
    allow_global=system,
    selective_upgrade=selective_upgrade,
    no_deps=False,
    pre=pre,
    dev=dev,
    requirements_dir=requirements_directory,
    index=index_url,
    pypi_mirror=pypi_mirror,
    use_constraint=True,
    )
    if c.returncode:
    sp.write_err(
    "{} An error occurred while installing {}!".format(
    click.style("Error: ", fg="red", bold=True),
    click.style(pkg_line, fg="green"),
    ),
    )
    sp.write_err(f"Error text: {c.stdout}")
    sp.write_err(click.style(format_pip_error(c.stderr), fg="cyan"))
    if project.s.is_verbose():
    sp.write_err(
    click.style(format_pip_output(c.stdout), fg="cyan")
    )
    if "setup.py egg_info" in c.stderr:
    sp.write_err(
    "This is likely caused by a bug in {}. "
    "Report this to its maintainers.".format(
    click.style(pkg_requirement.name, fg="green")
    )
    )
    sp.red.fail(
    environments.PIPENV_SPINNER_FAIL_TEXT.format(
    "Installation Failed"
    )
    )
    sys.exit(1)
    except (ValueError, RuntimeError) as e:
    sp.write_err("{}: {}".format(click.style("WARNING", fg="red"), e))
    sp.red.fail(
    environments.PIPENV_SPINNER_FAIL_TEXT.format(
    "Installation Failed",
    )
    )
    sys.exit(1)
    # Warn if --editable wasn't passed.
    if (
    pkg_requirement.is_vcs
    and not pkg_requirement.editable
    and not project.s.PIPENV_RESOLVE_VCS
    ):
    sp.write_err(
    "{}: You installed a VCS dependency in non-editable mode. "
    "This will work fine, but sub-dependencies will not be resolved by {}."
    "\n To enable this sub-dependency functionality, specify that this dependency is editable."
    "".format(
    click.style("Warning", fg="red", bold=True),
    click.style("$ pipenv lock", fg="yellow"),
    )
    )
    sp.write(
    "{} {} {} {}{}".format(
    click.style("Adding", bold=True),
    click.style(f"{pkg_requirement.name}", fg="green", bold=True),
    click.style("to Pipfile's", bold=True),
    click.style(
    "[dev-packages]" if dev else "[packages]",
    fg="yellow",
    bold=True,
    ),
    click.style(fix_utf8("..."), bold=True),
    )
    )
    # Add the package to the Pipfile.
    if index_url:
    index_name = project.add_index_to_pipfile(
    index_url, verify_ssl=index_url.startswith("https:")
    )
    pkg_requirement.index = index_name
    try:
    project.add_package_to_pipfile(pkg_requirement, dev)
    except ValueError:
    import traceback
    sp.write_err(
    "{} {}".format(
    click.style("Error:", fg="red", bold=True),
    traceback.format_exc(),
    )
    )
    sp.fail(
    environments.PIPENV_SPINNER_FAIL_TEXT.format(
    "Failed adding package to Pipfile"
    )
    )
    sp.ok(
    environments.PIPENV_SPINNER_OK_TEXT.format("Installation Succeeded")
    )
    # Update project settings with pre preference.
    if pre:
    project.update_settings({"allow_prereleases": pre})

    So pip_install should be replaced by another check_installable_package method. After that, resolving all the packages with do_lock (which use the first constraints file) will give us the same result.
    I didn't use this approach because:
    • $ pip search is not working, I could not find a way to check if a package is installable without calling $ pip install
    • The loop locks scary.

Copy link
Contributor Author

@dqkqd dqkqd Aug 9, 2022

Choose a reason for hiding this comment

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

Also, there is another case where resolving dependencies inside pip_install is necessary.
For example, I have packages installed with this Pipfile:

[packages]
django = "<3.0"

Suppose I install new dev package using command $ pipenv install --dev "django>3.0,<4.0". Clearly the package is installable but it shouldn't be, because it would conflict with default packages.

# See https://github.com/pypa/pipenv/issues/4371
# See https://github.com/pypa/pipenv/issues/2987
with PipenvInstance(chdir=True) as p:
with open(p.pipfile_path, 'w') as f:
contents = """
[packages]
requests = "<=2.14.0"

[dev-packages]
requests = "*"
""".strip()
f.write(contents)

c = p.pipenv("lock --dev")
assert c.returncode == 0
assert "requests" in p.lockfile["default"]
assert p.lockfile["default"]["requests"]["version"] == "==2.14.0"
assert "requests" in p.lockfile["develop"]
assert p.lockfile["develop"]["requests"]["version"] == "==2.14.0"

# requests 2.14.0 doesn't require these packages
assert "idna" not in p.lockfile["develop"]
assert "certifi" not in p.lockfile["develop"]
assert "urllib3" not in p.lockfile["develop"]
assert "chardet" not in p.lockfile["develop"]

c = p.pipenv("install --dev")
c = p.pipenv("run python -c 'import urllib3'")
assert c.returncode != 0
Loading