Skip to content

Commit

Permalink
Don't give child deps of vcs deps auto-precedence
Browse files Browse the repository at this point in the history
- Stop preferring resolution of VCS dependencies in all cases
- Resolve vcs dependencies together with non-vcs dependencies
- Clarify blocking and no-deps logic
- Add artifacts and tests
- Add vendoring task for artifacts
- Clean up release tasks
- Fixes pypa#3296

Signed-off-by: Dan Ryan <dan@danryan.co>
  • Loading branch information
techalchemy committed Nov 24, 2018
1 parent 3ce1394 commit a08a2da
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 76 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Expand Up @@ -22,3 +22,6 @@
[submodule "tests/test_artifacts/git/flask"]
path = tests/test_artifacts/git/flask
url = https://github.com/pallets/flask.git
[submodule "tests/test_artifacts/git/requests-2.18.4"]
path = tests/test_artifacts/git/requests-2.18.4
url = https://github.com/requests/requests
1 change: 1 addition & 0 deletions news/3296.bugfix.rst
@@ -0,0 +1 @@
Pipenv will now respect top-level pins over VCS dependency locks.
16 changes: 12 additions & 4 deletions pipenv/core.py
Expand Up @@ -734,25 +734,33 @@ def batch_install(deps_list, procs, failed_deps_queue,
os.environ["PIP_USER"] = vistir.compat.fs_str("0")
if "PYTHONHOME" in os.environ:
del os.environ["PYTHONHOME"]
if no_deps:
link = getattr(dep.req, "link", None)
is_wheel = False
if link:
is_wheel = link.is_wheel
is_non_editable_vcs = (dep.is_vcs and not dep.editable)
no_deps = not (dep.is_file_or_url and not (is_wheel or dep.editable))
block = any([dep.editable, dep.is_vcs, blocking])
c = pip_install(
dep,
ignore_hashes=any([ignore_hashes, dep.editable, dep.is_vcs]),
allow_global=allow_global,
no_deps=False if is_artifact else no_deps,
block=any([dep.editable, dep.is_vcs, blocking]),
no_deps=no_deps,
block=block,
index=index,
requirements_dir=requirements_dir,
pypi_mirror=pypi_mirror,
trusted_hosts=trusted_hosts,
extra_indexes=extra_indexes
)
if dep.is_vcs:
if dep.is_vcs or block:
c.block()
if procs.qsize() < nprocs:
c.dep = dep
procs.put(c)

if procs.full() or procs.qsize() == len(deps_list):
if procs.full() or procs.qsize() == len(deps_list) or block:
_cleanup_procs(procs, not blocking, failed_deps_queue, retry=retry)


Expand Down
51 changes: 26 additions & 25 deletions pipenv/utils.py
Expand Up @@ -540,7 +540,10 @@ def resolve(cmd, sp):
return c


def get_locked_dep(dep, pipfile_section):
def get_locked_dep(dep, pipfile_section, prefer_pipfile=False):
# the prefer pipfile flag is not used yet, but we are introducing
# it now for development purposes
# TODO: Is this implementation clear? How can it be improved?
entry = None
cleaner_kwargs = {
"is_top_level": False,
Expand All @@ -554,6 +557,14 @@ def get_locked_dep(dep, pipfile_section):
if entry:
cleaner_kwargs.update({"is_top_level": True, "pipfile_entry": entry})
lockfile_entry = clean_resolved_dep(dep, **cleaner_kwargs)
if entry and isinstance(entry, Mapping):
version = entry.get("version", "") if entry else ""
else:
version = entry if entry else ""
lockfile_version = lockfile_entry.get("version", "")
# Keep pins from the lockfile
if prefer_pipfile and lockfile_version != version and version.startswith("=="):
lockfile_version = version
return lockfile_entry


Expand Down Expand Up @@ -592,9 +603,17 @@ def venv_resolve_deps(
pipfile_section = "dev_packages" if dev else "packages"
lockfile_section = "develop" if dev else "default"
vcs_section = "vcs_{0}".format(pipfile_section)
vcs_deps = getattr(project, vcs_section, [])
if not deps and not vcs_deps:
editable_section = "editable_{0}".format(pipfile_section)
vcs_deps = getattr(project, vcs_section, {})
editable_deps = {
k: v for k, v in getattr(project, editable_section, {}).items()
if k not in vcs_deps
}
if not deps and not vcs_deps and not editable_deps:
return {}
editable_deps = convert_deps_to_pip(
editable_deps, project, r=False, include_index=True
)

if not pipfile:
pipfile = getattr(project, pipfile_section, None)
Expand All @@ -612,6 +631,7 @@ def venv_resolve_deps(
dev=dev,
)
vcs_deps = [req.as_line() for req in vcs_reqs if req.editable]
deps = set(deps) | set(vcs_deps) | set(editable_deps)
cmd = [
which("python", allow_global=allow_global),
Path(resolver.__file__.rstrip("co")).as_posix()
Expand All @@ -633,36 +653,17 @@ def venv_resolve_deps(
with create_spinner(text=fs_str("Locking...")) as sp:
c = resolve(cmd, sp)
results = c.out
if vcs_deps:
with temp_environ():
os.environ["PIPENV_PACKAGES"] = str("\n".join(vcs_deps))
sp.text = to_native_string("Locking VCS Dependencies...")
vcs_c = resolve(cmd, sp)
vcs_results, vcs_err = vcs_c.out, vcs_c.err
else:
vcs_results, vcs_err = "", ""
sp.green.ok(environments.PIPENV_SPINNER_OK_TEXT.format("Success!"))
outputs = [results, vcs_results]
if environments.is_verbose():
for output in outputs:
click_echo(output.split("RESULTS:")[0], err=True)
click_echo(results.split("RESULTS:")[0], err=True)
try:
results = json.loads(results.split("RESULTS:")[1].strip())
if vcs_results:
# For vcs dependencies, treat the initial pass at locking (i.e. checkout)
# as the pipfile entry because it gets us an actual ref to use
vcs_results = json.loads(vcs_results.split("RESULTS:")[1].strip())
vcs_lockfile = prepare_lockfile(vcs_results, vcs_lockfile.copy(), vcs_lockfile)
else:
vcs_results = []

except (IndexError, JSONDecodeError):
for out, err in [(c.out, c.err), (vcs_results, vcs_err)]:
click_echo(out.strip(), err=True)
click_echo(err.strip(), err=True)
click_echo(out.strip(), err=True)
click_echo(err.strip(), err=True)
raise RuntimeError("There was a problem with locking.")
lockfile[lockfile_section] = prepare_lockfile(results, pipfile, lockfile[lockfile_section])
lockfile[lockfile_section].update(vcs_lockfile)


def resolve_deps(
Expand Down
10 changes: 1 addition & 9 deletions tasks/__init__.py
Expand Up @@ -11,12 +11,4 @@
ROOT = Path(".").parent.parent.absolute()


@invoke.task
def clean_mdchangelog(ctx):
changelog = ROOT / "CHANGELOG.md"
content = changelog.read_text()
content = re.sub(r"([^\n]+)\n?\s+\[[\\]+(#\d+)\]\(https://github\.com/pypa/[\w\-]+/issues/\d+\)", r"\1 \2", content, flags=re.MULTILINE)
changelog.write_text(content)


ns = invoke.Collection(vendoring, release, clean_mdchangelog, vendor_passa.vendor_passa)
ns = invoke.Collection(vendoring, release, release.clean_mdchangelog, vendor_passa.vendor_passa)
164 changes: 127 additions & 37 deletions tasks/release.py
Expand Up @@ -5,9 +5,16 @@
from pipenv.__version__ import __version__
from parver import Version
from .vendoring import _get_git_root, drop_dir
import pathlib
from towncrier._builder import (
find_fragments, render_fragments, split_fragments,
)
from towncrier._settings import load_config


VERSION_FILE = 'pipenv/__version__.py'
ROOT = pathlib.Path(".").parent.parent.absolute()
PACKAGE_NAME = "pipenv"


def log(msg):
Expand All @@ -18,6 +25,15 @@ def get_version_file(ctx):
return _get_git_root(ctx).joinpath(VERSION_FILE)


def find_version(ctx):
version_file = get_version_file(ctx).read_text()
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")


def get_history_file(ctx):
return _get_git_root(ctx).joinpath('HISTORY.txt')

Expand All @@ -30,6 +46,66 @@ def get_build_dir(ctx):
return _get_git_root(ctx) / 'build'


def _render_log():
"""Totally tap into Towncrier internals to get an in-memory result.
"""
config = load_config(ROOT)
definitions = config['types']
fragments, fragment_filenames = find_fragments(
pathlib.Path(config['directory']).absolute(),
config['sections'],
None,
definitions,
)
rendered = render_fragments(
pathlib.Path(config['template']).read_text(encoding='utf-8'),
config['issue_format'],
split_fragments(fragments, definitions),
definitions,
config['underlines'][1:],
False, # Don't add newlines to wrapped text.
)
return rendered


@invoke.task
def release(ctx, dry_run=False):
drop_dist_dirs(ctx)
bump_version(ctx)
version = find_version(ctx)
tag_content = _render_log()
if dry_run:
ctx.run('towncrier --draft')
log('would remove: news/*')
log('would remove: CHANGELOG.draft.rst')
log(f'Would commit with message: "Release v{version}"')
else:
ctx.run('towncrier')
ctx.run("git add CHANGELOG.rst news/")
ctx.run("git rm CHANGELOG.draft.rst")
ctx.run(f'git commit -m "Release v{version}"')

tag_content = tag_content.replace('"', '\\"')
if dry_run:
log("Generated tag content: f{tag_content}")
markdown = ctx.run("towncrier --draft | pandoc -f rst -t markdown -o CHANGELOG.md", hide=True).stdout.strip()
content = clean_mdchangelog(ctx, markdown)
log(f"would generate markdown: {content}")
else:
generate_markdown(ctx)
clean_mdchangelog(ctx)
ctx.run(f'git tag -a v{version} -m "Version v{version}\n\n{tag_content}"')
build_dists(ctx)
if dry_run:
dist_pattern = f'{PACKAGE_NAME.replace("-", "[-_]")}-*'
artifacts = list(ROOT.joinpath('dist').glob(dist_pattern))
filename_display = '\n'.join(f' {a}' for a in artifacts)
log(f"Would upload dists: {filename_display}")
else:
upload_dists(ctx)
bump_version(ctx, dev=True)


def drop_dist_dirs(ctx):
log('Dropping Dist dir...')
drop_dir(get_dist_dir(ctx))
Expand All @@ -41,19 +117,32 @@ def drop_dist_dirs(ctx):
def build_dists(ctx):
drop_dist_dirs(ctx)
log('Building sdist using %s ....' % sys.executable)
for py_version in ['2.7', '3.6', '3.7']:
for py_version in ['3.6', '2.7']:
env = {'PIPENV_PYTHON': py_version}
ctx.run('pipenv install --dev', env=env)
ctx.run('pipenv run pip install -e . --upgrade --upgrade-strategy=eager', env=env)
if py_version == '3.6':
ctx.run('pipenv run python setup.py sdist', env=env)
ctx.run('pipenv run python setup.py sdist bdist_wheel', env=env)
else:
ctx.run('pipenv run python setup.py bdist_wheel', env=env)
log('Building wheel using python %s ....' % py_version)
ctx.run('pipenv run python setup.py bdist_wheel', env=env)


@invoke.task(build_dists)
def upload_dists(ctx):
log('Uploading distributions to pypi...')
ctx.run('twine upload dist/*')
def upload_dists(ctx, repo="pypi"):
dist_pattern = f'{PACKAGE_NAME.replace("-", "[-_]")}-*'
artifacts = list(ROOT.joinpath('dist').glob(dist_pattern))
filename_display = '\n'.join(f' {a}' for a in artifacts)
print(f'[release] Will upload:\n{filename_display}')
try:
input('[release] Release ready. ENTER to upload, CTRL-C to abort: ')
except KeyboardInterrupt:
print('\nAborted!')
return

arg_display = ' '.join(f'"{n}"' for n in artifacts)
ctx.run(f'twine upload --repository="{repo}" {arg_display}')



@invoke.task
Expand All @@ -69,67 +158,68 @@ def generate_changelog(ctx, commit=False, draft=False):
commit = False
log('Writing draft to file...')
ctx.run('towncrier --draft > CHANGELOG.draft.rst')
if commit:
else:
ctx.run('towncrier')
if commit:
log('Committing...')
ctx.run('git add CHANGELOG.rst')
ctx.run('git rm CHANGELOG.draft.rst')
ctx.run('git commit -m "Update changelog."')


@invoke.task
def clean_mdchangelog(ctx, content=None):
changelog = None
if not content:
changelog = _get_git_root(ctx) / "CHANGELOG.md"
content = changelog.read_text()
content = re.sub(r"([^\n]+)\n?\s+\[[\\]+(#\d+)\]\(https://github\.com/pypa/[\w\-]+/issues/\d+\)", r"\1 \2", content, flags=re.MULTILINE)
if changelog:
changelog.write_text(content)
else:
return content


@invoke.task
def tag_version(ctx, push=False):
version = Version.parse(__version__)
log('Tagging revision: v%s' % version)
ctx.run('git tag v%s' % version)
version = find_version(ctx)
version = Version.parse(version)
log('Tagging revision: v%s' % version.normalize())
ctx.run('git tag v%s' % version.normalize())
if push:
log('Pushing tags...')
ctx.run('git push origin master')
ctx.run('git push --tags')


@invoke.task
def bump_version(ctx, dry_run=False, increment=True, release=False, dev=False, pre=False, tag=None, clear=False, commit=False,):
def bump_version(ctx, dry_run=False, dev=False, pre=False, tag=None, commit=False):
current_version = Version.parse(__version__)
today = datetime.date.today()
next_month_number = today.month + 1 if today.month != 12 else 1
next_year_number = today.year if next_month_number != 1 else today.year+1
next_month = (next_year_number, next_month_number, 0)
tomorrow = today + datetime.timedelta(days=1)
next_month = today + datetime.timedelta(months=1)
next_year = today + datetime.timedelta(years=1)
if pre and not tag:
print('Using "pre" requires a corresponding tag.')
return
if release and not dev and not pre and increment:
if not (dev or pre or tag):
new_version = current_version.replace(release=today.timetuple()[:3]).clear(pre=True, dev=True)
elif release and (dev or pre):
if increment:
new_version = current_version.replace(release=today.timetuple()[:3])
else:
new_version = current_version
if pre and dev:
raise RuntimeError("Can't use 'pre' and 'dev' together!")
if dev or pre:
new_version = current_version.replace(release=tomorrow.timetuple()[:3]).clear(pre=True, dev=True)
if dev:
new_version = new_version.bump_dev()
elif pre:
new_version = new_version.bump_pre(tag=tag)
else:
if not release:
increment = False
if increment:
new_version = current_version.replace(release=next_month)
else:
new_version = current_version
if dev:
new_version = new_version.bump_dev()
elif pre:
new_version = new_version.bump_pre(tag=tag)
if clear:
new_version = new_version.clear(dev=True, pre=True, post=True)
log('Updating version to %s' % new_version.normalize())
version_file = get_version_file(ctx)
file_contents = version_file.read_text()
log('Found current version: %s' % __version__)
version = find_version(ctx)
log('Found current version: %s' % version)
if dry_run:
log('Would update to: %s' % new_version.normalize())
else:
log('Updating to: %s' % new_version.normalize())
version_file.write_text(file_contents.replace(__version__, str(new_version.normalize())))
version_file.write_text(file_contents.replace(version, str(new_version.normalize())))
if commit:
ctx.run('git add {0}'.format(version_file))
log('Committing...')
Expand Down

0 comments on commit a08a2da

Please sign in to comment.