Skip to content

Commit

Permalink
Simplify and add testing of update-egg-info
Browse files Browse the repository at this point in the history
Make use of existing setuptools_scm git code
Add GitWorkdir.get_git_dir to find the .git directory
Correct the environment variable name
Improve documentation of limitations
Add update-egg-info testing
  • Loading branch information
MHendricks committed Oct 24, 2021
1 parent f06cc7a commit 5f83bdc
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 106 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
exclude *.nix
exclude .pre-commit-config.yaml
include .pre-commit-hooks.yaml
include *.py
include testing/*.py
include tox.ini
Expand Down
37 changes: 25 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,13 @@ Environment variables
when defined, a ``os.pathsep`` separated list
of directory names to ignore for root finding


:SETUPTOOLS_SCM_SKIP_UPDATE_EGG_INFO:
when set to 1, skip running the build command if the ``update-egg-info`` pre-commit
hooks are installed. Use this if you are doing a lot of git operations and don't
want to wait for the somewhat expensive build command to be run after each of them
without uninstalling those pre-commit hooks.

Extending setuptools_scm
------------------------

Expand Down Expand Up @@ -586,19 +593,25 @@ its still possible to install a more recent version of setuptools in order to ha
and/or install the package by using wheels or eggs.


Automatic Update of Editable Install Version
--------------------------------------------
When developing a pip package you often will use an editable install of your pip package
Automatic Update of Editable Installs Version
---------------------------------------------
While developing a pip package you often will use an editable install of your package
to make development easier. If you have complex dependencies you may run into
``VersionConflict`` errors when another package updates its minimum version of a package you
have editable installed.
``VersionConflict`` errors when another package updates its requirements when using
``pkg_resources`` to process entry points including console_scripts.

.. code-block: python
pkg_resources.VersionConflict: (My_Package 2.75.0.dev0+g54e73c49.d20211024 (c:\dev\my_package), Requirement.parse('My_Package>=0.0.14'))
You can use `pre-commit <https://pre-commit.com/>`_ to install several post hooks that
automatically call ``python setup.py egg_info``. If you are using setuptools_scm to
You can use `pre-commit <https://pre-commit.com/>`_ to install several post git hooks
that automatically call ``python setup.py egg_info``. If you are using setuptools_scm to
generate your version this will update the installed version for all editable installs
using this repo.

To configure your repo to make use of this, add this info to it's ``.pre-commit-config.yaml`` file.
To configure your repo to make use of this, add this info to it's
``.pre-commit-config.yaml`` file.

.. code-block:: yaml
Expand All @@ -609,22 +622,22 @@ To configure your repo to make use of this, add this info to it's ``.pre-commit-
- id: update-egg-info
- id: update-egg-info-rewrite
You will need to be using pre-commit >= 2.15.0. You will need to specify multiple hook-type
arguments to install hooks.
You will need to be using pre-commit >= 2.15.0. You will need to specify multiple
hook-type arguments to install the required hooks.

.. code-block:: shell
$ pre-commit install -t post-commit -t post-merge -t post-checkout -t post-rewrite
While this is installed any time you commit, merge, rebase, checkout changes on your git
repo, it the version of your editable pip package will be up to date.
repo, the version of your editable pip package will be kept up to date.


Customizing the Build Command
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default update-egg-info will use the command ``python setup.py egg_info``, this takes
the least amount of time to update the version metadata for a standard setuptools
the least amount of time to update the version metadata for a standard setuptools_scm
editable install to update the version information. However if you need to customize the
build command you can pass a custom build command to each hook.

Expand Down
5 changes: 5 additions & 0 deletions src/setuptools_scm/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ def get_branch(self):
branch = None
return branch

def get_git_dir(self):
"""Returns the absolute path to the .git directory"""
out, err, ret = self.do_ex("git rev-parse --absolute-git-dir")
return out.strip()

def get_head_date(self):
timestamp, err, ret = self.do_ex("git log -n 1 HEAD --format=%cI")
if ret:
Expand Down
168 changes: 74 additions & 94 deletions src/setuptools_scm/pre_commit_hook.py
Original file line number Diff line number Diff line change
@@ -1,107 +1,81 @@
import glob
import os
import subprocess
import sys

from setuptools_scm.git import GitWorkdir


class PreCommitHook:
def __init__(self, repo_dir, build_command):
self._git_repo_dir = None
self._is_git_repo = None
self.build_command = build_command
self.repo_dir = repo_dir

def call_subprocess(self, cmd, env=None, cwd=None, communicate=True):
kwargs = {
"bufsize": 1,
"stdout": subprocess.PIPE,
"stderr": subprocess.STDOUT,
"universal_newlines": True,
}
if env:
kwargs["env"] = env
if not cwd:
kwargs["cwd"] = self.repo_dir

if sys.platform == "win32" and "pythonw" in sys.executable.lower():
# Windows 7 has an issue with making subprocesses
# if the stdin handle is None, so pass the PIPE
kwargs["stdin"] = subprocess.PIPE
# Then, because there is no current window for stdout, any
# subprocesses will try to create a new window
# This constant comes from the WindowsAPI, but is not
# defined in subprocess until python 3.7
create_no_window = 0x08000000
kwargs["creationflags"] = create_no_window

proc = subprocess.Popen(cmd, **kwargs)

out = None
err = None
if communicate:
out, err = proc.communicate()
# Report any errors that happened while running the hook
if proc.returncode:
print(" stdout ".center(50, "-"))
print(out)
print(" stderr ".center(50, "-"))
print(err)
raise Exception(
"Error {} returned on on subprocess call: {}".format(
proc.returncode, cmd
)
)
return proc, out, err

@property
def git_repo_dir(self):
"""Returns the root of the git repo if this is a git repo or None."""
if self._git_repo_dir is None and self.is_git_repo:
proc, output, _ = self.call_subprocess(
["git", "rev-parse", "--absolute-git-dir"]
)
self._git_repo_dir = output.strip()
return self._git_repo_dir
self.work_dir = GitWorkdir.from_potential_worktree(repo_dir)

def is_git_rebasing(self):
"""Checks if git is currently in rebase mode."""
rebase_merge = os.path.join(self.git_repo_dir, "rebase-merge")
rebase_apply = os.path.join(self.git_repo_dir, "rebase-apply")
"""Checks if git is currently in rebase mode and returns the output of build_command.
This detection code could be improved, Here is a summary of how well this
handles various git operations.
Rebase: A series of post-commit hooks where `is_git_rebasing` returns true,
and finally the post-rewrite hook is called. This is why we have a separate
`update-egg-info-rewrite` hook that passes `--post-rewrite` to force the
running of build_command.
Squash: For each commit being squashed, `is_git_rebasing` returns true for
the post-commit hook, but the post-rewrite hook is called several times,
calling build_command each of those times.
Amend: This ends up calling build_command twice.
"""
git_dir = self.work_dir.get_git_dir()
rebase_merge = os.path.join(git_dir, "rebase-merge")
rebase_apply = os.path.join(git_dir, "rebase-apply")
return os.path.exists(rebase_merge) or os.path.exists(rebase_apply)

def is_git_repo(self):
if self._is_git_repo is None:
proc, output, _ = self.call_subprocess(
["git", "rev-parse", "--is-inside-work-tree"]
def update_egg_info(self, force_on_rebase=False):
"""Run the `python setup.py egg_info` if this is a editable install."""

# Allow users to temporarily disable the hook. `is_git_rebasing` doesn't work
# in all cases and there may be cases where you want to disable this hook to
# speed up git operations. Running egg_info adds some slowness to the git
# commands, especially for large packages with a lot of files.
if os.getenv("SETUPTOOLS_SCM_SKIP_UPDATE_EGG_INFO", "0") != "0":
output = (
'update-egg-info: Skipping, "SETUPTOOLS_SCM_SKIP_UPDATE_EGG_INFO" '
"env var set."
)
self._is_git_repo = proc.output.strip() == "true"
if None is self._is_git_repo:
raise OSError("git rev-parse error")
return self._is_git_repo
return output, 0

def run_egg_info(self):
"""Run the `python setup.py egg_info` if this is a editable install."""
# If this is not an editable install there is no need to run it
contents = os.listdir(self.work_dir.path)

if not os.path.exists(os.path.join(self.repo_dir, "setup.py")):
# This is not a setup.py pip package so there is nothing to do.
return False
# If there is not a setup.cfg or setup.py file then this can't be an editable
# install, no need to try to run build_command.
if "setup.cfg" not in contents and "setup.py" not in contents:
output = "update-egg-info: Skipping, no setup script found."
return output, 0

egg_info = os.path.join(self.repo_dir, "*.egg-info")
# If a egg_info directory doesn't exist its not currently an editable install
# don't turn it into one.
if not any(filter(lambda i: os.path.splitext(i)[-1] == ".egg-info", contents)):
output = "update-egg-info: Skipping, no .egg-info directory found."
return output, 0

if not glob.glob(egg_info):
# This package is not currently a editable install, so there is no need
# to update the .egg-info folder.
return False
# Check if git is currently rebasing, if so and force_on_rebase is False, this
# is likely a post-commit call, and the post-rewrite hook will be called later,
# skip running build_command for now.
if not force_on_rebase and self.is_git_rebasing():
output = "update-egg-info: Skipping, rebase in progress."
return output, 0

# This environment variable prevents setup.py from running this recursively
if "PILLAR_VERSION_NO_EGG_INFO" not in os.environ:
env = dict(os.environ)
env["PILLAR_VERSION_NO_EGG_INFO"] = "1"
# Run the build command
output = [f"update-egg-info: Running command: {self.build_command}"]
cmd_out, error, returncode = self.work_dir.do_ex(self.build_command)

proc, output, error = self.call_subprocess(self.build_command, env=env)
if cmd_out:
output += cmd_out
if returncode and error:
output.append("update-egg-info: Error running build_command:")
output.append(error)

return proc.returncode
return 0
output = "\n".join(output)
return output, returncode


def main():
Expand All @@ -113,7 +87,15 @@ def main():
"editable installs."
)
)
parser.add_argument("--post-rewrite", action="store_true")
parser.add_argument(
"--post-rewrite",
action="store_true",
help=(
"Force running `command` after a rebase. update-egg-info skips calling "
"`command` if it detects that git is performing a rebase as generating the"
"egg_info takes extra time, especially for larger packages."
),
)
parser.add_argument(
"command",
default=["python", "setup.py", "egg_info"],
Expand All @@ -126,14 +108,12 @@ def main():
args = parser.parse_args()

hook = PreCommitHook(os.getcwd(), build_command=args.command)
# TODO: look at the contents of .git to see if we can auto-detect a post-rewrite
# print(os.listdir(r'C:\blur\dev\chernobyl\.git'))
if not args.post_rewrite and hook.is_git_rebasing():
print("It's a rebase, skipping.")
return

print("Running run_egg_info.")
sys.exit(hook.run_egg_info())
output, returncode = hook.update_egg_info(force_on_rebase=args.post_rewrite)

print(output)

# Return the error code so pre-commit can report the failure.
sys.exit(returncode)


if __name__ == "__main__":
Expand Down
Loading

0 comments on commit 5f83bdc

Please sign in to comment.