diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..f53718251 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# migrate code to black +f922e2e1e60e3a964dbd07597e7c44c068b7ba1d diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6af458af..608bfa7dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.5', '3.6', '3.7', '3.8'] + python-version: ['3.6', '3.7', '3.8', '3.9.0-rc.1'] steps: - name: Checkout uses: actions/checkout@v2 @@ -58,7 +58,7 @@ jobs: - name: Install dependencies run: | pip install wheel - pip install --upgrade --upgrade-strategy=eager pytest pytest-asyncio "jupyterlab~=2.0" + pip install --upgrade --upgrade-strategy=eager "jupyterlab~=2.0" - name: Test the extension run: | @@ -71,6 +71,9 @@ jobs: pip install jupyterlab_git[test] --pre --no-index --find-links=dist --no-deps --no-cache-dir -v # Install the extension dependencies based on the current setup.py pip install jupyterlab_git[test] + + # Python formatting checks + black . --check # Run the Python tests pytest jupyterlab_git -r ap diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..1656e9544 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/psf/black + rev: 20.8b1 # Replace by any tag/version: https://github.com/psf/black/tags + hooks: + - id: black + language_version: python3 # Should be a command that runs python3.6+ \ No newline at end of file diff --git a/README.md b/README.md index f3ef9dcb0..e580f141c 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ cd jupyterlab-git # Install the server extension in development mode and enable it pip install -e .[test] +pre-commit install jupyter serverextension enable --py jupyterlab_git --sys-prefix # Build and install your development version of the extension diff --git a/jupyterlab_git/__init__.py b/jupyterlab_git/__init__.py index adc977a2e..34fa629c1 100644 --- a/jupyterlab_git/__init__.py +++ b/jupyterlab_git/__init__.py @@ -8,6 +8,7 @@ from jupyterlab_git.handlers import setup_handlers from jupyterlab_git.git import Git + class JupyterLabGit(Configurable): """ Config options for jupyterlab_git @@ -16,26 +17,24 @@ class JupyterLabGit(Configurable): """ actions = Dict( - help='Actions to be taken after a git command. Each action takes a list of commands to execute (strings). Supported actions: post_init', + help="Actions to be taken after a git command. Each action takes a list of commands to execute (strings). Supported actions: post_init", config=True, trait=List( - trait=Unicode(), - help='List of commands to run. E.g. ["touch baz.py"]' + trait=Unicode(), help='List of commands to run. E.g. ["touch baz.py"]' ) # TODO Validate ) + def _jupyter_server_extension_paths(): - """Declare the Jupyter server extension paths. - """ + """Declare the Jupyter server extension paths.""" return [{"module": "jupyterlab_git"}] def load_jupyter_server_extension(nbapp): - """Load the Jupyter server extension. - """ + """Load the Jupyter server extension.""" config = JupyterLabGit(config=nbapp.config) - git = Git(nbapp.web_app.settings['contents_manager'], config) + git = Git(nbapp.web_app.settings["contents_manager"], config) nbapp.web_app.settings["git"] = git setup_handlers(nbapp.web_app) diff --git a/jupyterlab_git/_version.py b/jupyterlab_git/_version.py index b3a698a0d..051378e80 100644 --- a/jupyterlab_git/_version.py +++ b/jupyterlab_git/_version.py @@ -1,7 +1,7 @@ # Copyright (c) Project Jupyter. # Distributed under the terms of the Modified BSD License. -version_info = (0, 21, 1) -flag = '' +version_info = (0, 22, 0) +flag = "a0" __version__ = ".".join(map(str, version_info)) + flag diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 095dd3c9c..32d148a1f 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -33,6 +33,7 @@ execution_lock = tornado.locks.Lock() + async def execute( cmdline: "List[str]", cwd: "str", @@ -61,7 +62,12 @@ async def call_subprocess_with_authentication( ) -> "Tuple[int, str, str]": try: p = pexpect.spawn( - cmdline[0], cmdline[1:], cwd=cwd, env=env, encoding="utf-8", timeout=None + cmdline[0], + cmdline[1:], + cwd=cwd, + env=env, + encoding="utf-8", + timeout=None, ) # We expect a prompt from git @@ -101,14 +107,16 @@ def call_subprocess( return (process.returncode, output.decode("utf-8"), error.decode("utf-8")) try: - await execution_lock.acquire(timeout=datetime.timedelta(seconds=MAX_WAIT_FOR_EXECUTE_S)) + await execution_lock.acquire( + timeout=datetime.timedelta(seconds=MAX_WAIT_FOR_EXECUTE_S) + ) except tornado.util.TimeoutError: return (1, "", "Unable to get the lock on the directory") try: # Ensure our execution operation will succeed by first checking and waiting for the lock to be removed time_slept = 0 - lockfile = os.path.join(cwd, '.git', 'index.lock') + lockfile = os.path.join(cwd, ".git", "index.lock") while os.path.exists(lockfile) and time_slept < MAX_WAIT_FOR_LOCK_S: await tornado.gen.sleep(CHECK_LOCK_INTERVAL_S) time_slept += CHECK_LOCK_INTERVAL_S @@ -129,9 +137,15 @@ def call_subprocess( code, output, error = await current_loop.run_in_executor( None, call_subprocess, cmdline, cwd, env ) - log_output = output[:MAX_LOG_OUTPUT] + "..." if len(output) > MAX_LOG_OUTPUT else output - log_error = error[:MAX_LOG_OUTPUT] + "..." if len(error) > MAX_LOG_OUTPUT else error - get_logger().debug("Code: {}\nOutput: {}\nError: {}".format(code, log_output, log_error)) + log_output = ( + output[:MAX_LOG_OUTPUT] + "..." if len(output) > MAX_LOG_OUTPUT else output + ) + log_error = ( + error[:MAX_LOG_OUTPUT] + "..." if len(error) > MAX_LOG_OUTPUT else error + ) + get_logger().debug( + "Code: {}\nOutput: {}\nError: {}".format(code, log_output, log_error) + ) except BaseException: get_logger().warning("Fail to execute {!s}".format(cmdline), exc_info=True) finally: @@ -188,11 +202,13 @@ async def config(self, top_repo_path, **kwargs): else: raw = output.strip() s = CONFIG_PATTERN.split(raw) - response["options"] = {k:v for k, v in zip(s[1::2], s[2::2])} + response["options"] = {k: v for k, v in zip(s[1::2], s[2::2])} return response - async def changed_files(self, current_path, base=None, remote=None, single_commit=None): + async def changed_files( + self, current_path, base=None, remote=None, single_commit=None + ): """Gets the list of changed files between two Git refs, or the files changed in a single commit There are two reserved "refs" for the base @@ -227,7 +243,9 @@ async def changed_files(self, current_path, base=None, remote=None, single_commi response = {} try: - code, output, error = await execute(cmd, cwd=os.path.join(self.root_dir, current_path)) + code, output, error = await execute( + cmd, cwd=os.path.join(self.root_dir, current_path) + ) except subprocess.CalledProcessError as e: response["code"] = e.returncode response["message"] = e.output.decode("utf-8") @@ -282,7 +300,8 @@ async def status(self, current_path): """ cmd = ["git", "status", "--porcelain", "-u", "-z"] code, my_output, my_error = await execute( - cmd, cwd=os.path.join(self.root_dir, current_path), + cmd, + cwd=os.path.join(self.root_dir, current_path), ) if code != 0: @@ -299,10 +318,11 @@ async def status(self, current_path): "--numstat", "-z", "--cached", - "4b825dc642cb6eb9a060e54bf8d69288fbee4904" + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", ] text_code, text_output, _ = await execute( - command, cwd=os.path.join(self.root_dir, current_path), + command, + cwd=os.path.join(self.root_dir, current_path), ) are_binary = dict() @@ -315,14 +335,16 @@ async def status(self, current_path): line_iterable = (line for line in strip_and_split(my_output) if line) for line in line_iterable: name = line[3:] - result.append({ - "x": line[0], - "y": line[1], - "to": name, - # if file was renamed, next line contains original path - "from": next(line_iterable) if line[0]=='R' else name, - "is_binary": are_binary.get(name, None) - }) + result.append( + { + "x": line[0], + "y": line[1], + "to": name, + # if file was renamed, next line contains original path + "from": next(line_iterable) if line[0] == "R" else name, + "is_binary": are_binary.get(name, None), + } + ) return {"code": code, "files": result} async def log(self, current_path, history_count=10): @@ -336,7 +358,8 @@ async def log(self, current_path, history_count=10): ("-%d" % history_count), ] code, my_output, my_error = await execute( - cmd, cwd=os.path.join(self.root_dir, current_path), + cmd, + cwd=os.path.join(self.root_dir, current_path), ) if code != 0: return {"code": code, "command": " ".join(cmd), "message": my_error} @@ -376,7 +399,8 @@ async def detailed_log(self, selected_hash, current_path): """ cmd = ["git", "log", "-1", "--numstat", "--oneline", "-z", selected_hash] code, my_output, my_error = await execute( - cmd, cwd=os.path.join(self.root_dir, current_path), + cmd, + cwd=os.path.join(self.root_dir, current_path), ) if code != 0: @@ -388,11 +412,11 @@ async def detailed_log(self, selected_hash, current_path): line_iterable = iter(strip_and_split(my_output)[1:]) for line in line_iterable: is_binary = line.startswith("-\t-\t") - insertions, deletions, file = line.split('\t') - insertions = insertions.replace('-', '0') - deletions = deletions.replace('-', '0') + insertions, deletions, file = line.split("\t") + insertions = insertions.replace("-", "0") + deletions = deletions.replace("-", "0") - if file == '': + if file == "": # file was renamed or moved, we need next two lines of output from_path = next(line_iterable) to_path = next(line_iterable) @@ -402,20 +426,23 @@ async def detailed_log(self, selected_hash, current_path): modified_file_name = file.split("/")[-1] modified_file_path = file - result.append({ - "modified_file_path": modified_file_path, - "modified_file_name": modified_file_name, - "insertion": insertions, - "deletion": deletions, - "is_binary": is_binary - }) + result.append( + { + "modified_file_path": modified_file_path, + "modified_file_name": modified_file_name, + "insertion": insertions, + "deletion": deletions, + "is_binary": is_binary, + } + ) total_insertions += int(insertions) total_deletions += int(deletions) modified_file_note = "{num_files} files changed, {insertions} insertions(+), {deletions} deletions(-)".format( num_files=len(result), insertions=total_insertions, - deletions=total_deletions) + deletions=total_deletions, + ) return { "code": code, @@ -559,9 +586,7 @@ async def branch_remotes(self, current_path): results = [] try: - for name, commit_sha in ( - line.split("\t") for line in output.splitlines() - ): + for name, commit_sha in (line.split("\t") for line in output.splitlines()): results.append( { "is_current_branch": False, @@ -664,7 +689,7 @@ async def add_all_untracked(self, top_repo_path): untracked = [] for f in status["files"]: - if f["x"]=="?" and f["y"]=="?": + if f["x"] == "?" and f["y"] == "?": untracked.append(f["from"].strip('"')) return await self.add(untracked, top_repo_path) @@ -721,7 +746,8 @@ async def checkout_new_branch(self, branchname, startpoint, current_path): """ cmd = ["git", "checkout", "-b", branchname, startpoint] code, my_output, my_error = await execute( - cmd, cwd=os.path.join(self.root_dir, current_path), + cmd, + cwd=os.path.join(self.root_dir, current_path), ) if code == 0: return {"code": code, "message": my_output} @@ -830,14 +856,19 @@ async def pull(self, curr_fb_path, auth=None, cancel_on_conflict=False): if code != 0: output = output.strip() - has_conflict = "automatic merge failed; fix conflicts and then commit the result." in output.lower() + has_conflict = ( + "automatic merge failed; fix conflicts and then commit the result." + in output.lower() + ) if cancel_on_conflict and has_conflict: code, _, error = await execute( ["git", "merge", "--abort"], - cwd=os.path.join(self.root_dir, curr_fb_path) + cwd=os.path.join(self.root_dir, curr_fb_path), ) if code == 0: - response["message"] = "Unable to pull latest changes as doing so would result in a merge conflict. In order to push your local changes, you may want to consider creating a new branch based on your current work and pushing the new branch. Provided your repository is hosted (e.g., on GitHub), once pushed, you can create a pull request against the original branch on the remote repository and manually resolve the conflicts during pull request review." + response[ + "message" + ] = "Unable to pull latest changes as doing so would result in a merge conflict. In order to push your local changes, you may want to consider creating a new branch based on your current work and pushing the new branch. Provided your repository is hosted (e.g., on GitHub), once pushed, you can create a pull request against the original branch on the remote repository and manually resolve the conflicts during pull request review." else: response["message"] = error.strip() elif has_conflict: @@ -850,7 +881,7 @@ async def pull(self, curr_fb_path, auth=None, cancel_on_conflict=False): async def push(self, remote, branch, curr_fb_path, auth=None, set_upstream=False): """ Execute `git push $UPSTREAM $BRANCH`. The choice of upstream and branch is up to the caller. - """ + """ command = ["git", "push"] if set_upstream: command.append("--set-upstream") @@ -887,16 +918,19 @@ async def init(self, current_path): """ cmd = ["git", "init"] cwd = os.path.join(self.root_dir, current_path) - code, _, error = await execute( - cmd, cwd=cwd - ) + code, _, error = await execute(cmd, cwd=cwd) actions = None if code == 0: - code, actions = await self._maybe_run_actions('post_init', cwd) + code, actions = await self._maybe_run_actions("post_init", cwd) if code != 0: - return {"code": code, "command": " ".join(cmd), "message": error, "actions": actions} + return { + "code": code, + "command": " ".join(cmd), + "message": error, + "actions": actions, + } return {"code": code, "actions": actions} async def _maybe_run_actions(self, name, cwd): @@ -908,24 +942,26 @@ async def _maybe_run_actions(self, name, cwd): for action in actions_list: try: # We trust the actions as they were passed via a config and not the UI - code, stdout, stderr = await execute( - shlex.split(action), cwd=cwd + code, stdout, stderr = await execute(shlex.split(action), cwd=cwd) + actions.append( + { + "cmd": action, + "code": code, + "stdout": stdout, + "stderr": stderr, + } ) - actions.append({ - 'cmd': action, - 'code': code, - 'stdout': stdout, - 'stderr': stderr - }) # After any failure, stop except Exception as e: code = 1 - actions.append({ - 'cmd': action, - 'code': 1, - 'stdout': None, - 'stderr': 'Exception: {}'.format(e) - }) + actions.append( + { + "cmd": action, + "code": 1, + "stdout": None, + "stderr": "Exception: {}".format(e), + } + ) if code != 0: break @@ -960,8 +996,7 @@ async def get_current_branch(self, current_path): ) async def _get_current_branch_detached(self, current_path): - """Execute 'git branch -a' to get current branch details in case of detached HEAD - """ + """Execute 'git branch -a' to get current branch details in case of detached HEAD""" command = ["git", "branch", "-a"] code, output, error = await execute( command, cwd=os.path.join(self.root_dir, current_path) @@ -1004,8 +1039,12 @@ async def get_upstream_branch(self, current_path, branch_name): return {"code": code, "command": " ".join(command), "message": error} remote_name = output.strip() - remote_branch = rev_parse_output.strip().lstrip(remote_name+"/") - return {"code": code, "remote_short_name": remote_name, "remote_branch": remote_branch} + remote_branch = rev_parse_output.strip().lstrip(remote_name + "/") + return { + "code": code, + "remote_short_name": remote_name, + "remote_branch": remote_branch, + } async def _get_tag(self, current_path, commit_sha): """Execute 'git describe commit_sha' to get @@ -1074,7 +1113,9 @@ async def diff_content(self, filename, prev_ref, curr_ref, top_repo_path): """ is_binary = await self._is_binary(filename, prev_ref["git"], top_repo_path) if is_binary: - raise tornado.web.HTTPError(log_message="Error occurred while executing command to retrieve plaintext diff as file is not UTF-8.") + raise tornado.web.HTTPError( + log_message="Error occurred while executing command to retrieve plaintext diff as file is not UTF-8." + ) prev_content = await self.show(filename, prev_ref["git"], top_repo_path) if "special" in curr_ref: @@ -1083,17 +1124,23 @@ async def diff_content(self, filename, prev_ref, curr_ref, top_repo_path): elif curr_ref["special"] == "INDEX": is_binary = await self._is_binary(filename, "INDEX", top_repo_path) if is_binary: - raise tornado.web.HTTPError(log_message="Error occurred while executing command to retrieve plaintext diff as file is not UTF-8.") + raise tornado.web.HTTPError( + log_message="Error occurred while executing command to retrieve plaintext diff as file is not UTF-8." + ) curr_content = await self.show(filename, "", top_repo_path) else: raise tornado.web.HTTPError( - log_message="Error while retrieving plaintext diff, unknown special ref '{}'.".format(curr_ref["special"]) + log_message="Error while retrieving plaintext diff, unknown special ref '{}'.".format( + curr_ref["special"] + ) ) else: is_binary = await self._is_binary(filename, curr_ref["git"], top_repo_path) if is_binary: - raise tornado.web.HTTPError(log_message="Error occurred while executing command to retrieve plaintext diff as file is not UTF-8.") + raise tornado.web.HTTPError( + log_message="Error occurred while executing command to retrieve plaintext diff as file is not UTF-8." + ) curr_content = await self.show(filename, curr_ref["git"], top_repo_path) @@ -1121,20 +1168,42 @@ async def _is_binary(self, filename, ref, top_repo_path): HTTPError: if git command failed """ if ref == "INDEX": - command = ["git", "diff", "--numstat", "--cached", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", "--", filename] + command = [ + "git", + "diff", + "--numstat", + "--cached", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "--", + filename, + ] else: - command = ["git", "diff", "--numstat", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", ref, "--", filename] # where 4b825... is a magic SHA which represents the empty tree + command = [ + "git", + "diff", + "--numstat", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + ref, + "--", + filename, + ] # where 4b825... is a magic SHA which represents the empty tree code, output, error = await execute(command, cwd=top_repo_path) if code != 0: - err_msg = "fatal: Path '{}' does not exist (neither on disk nor in the index)".format(filename).lower() + err_msg = "fatal: Path '{}' does not exist (neither on disk nor in the index)".format( + filename + ).lower() if err_msg in error.lower(): return False - raise tornado.web.HTTPError(log_message="Error while determining if file is binary or text '{}'.".format(error)) + raise tornado.web.HTTPError( + log_message="Error while determining if file is binary or text '{}'.".format( + error + ) + ) # For binary files, `--numstat` outputs two `-` characters separated by TABs: - return output.startswith('-\t-\t') + return output.startswith("-\t-\t") async def remote_add(self, top_repo_path, url, name=DEFAULT_REMOTE_NAME): """Handle call to `git remote add` command. @@ -1148,11 +1217,8 @@ async def remote_add(self, top_repo_path, url, name=DEFAULT_REMOTE_NAME): """ cmd = ["git", "remote", "add", name, url] code, _, error = await execute(cmd, cwd=top_repo_path) - response = { - "code": code, - "command": " ".join(cmd) - } - + response = {"code": code, "command": " ".join(cmd)} + if code != 0: response["message"] = error @@ -1162,16 +1228,13 @@ async def remote_show(self, path): """Handle call to `git remote show` command. Args: path (str): Git repository path - + Returns: List[str]: Known remotes """ command = ["git", "remote", "show"] code, output, error = await execute(command, cwd=path) - response = { - "code": code, - "command": " ".join(command) - } + response = {"code": code, "command": " ".join(command)} if code == 0: response["remotes"] = [r.strip() for r in output.splitlines()] else: @@ -1180,22 +1243,22 @@ async def remote_show(self, path): return response async def ensure_gitignore(self, top_repo_path): - """Handle call to ensure .gitignore file exists and the + """Handle call to ensure .gitignore file exists and the next append will be on a new line (this means an empty file or a file ending with \n). top_repo_path: str Top Git repository path """ - try: + try: gitignore = pathlib.Path(top_repo_path) / ".gitignore" if not gitignore.exists(): gitignore.touch() elif gitignore.stat().st_size > 0: content = gitignore.read_text() - if (content[-1] != "\n"): + if content[-1] != "\n": with gitignore.open("a") as f: - f.write('\n') + f.write("\n") except BaseException as error: return {"code": -1, "message": str(error)} return {"code": 0} @@ -1206,7 +1269,7 @@ async def ignore(self, top_repo_path, file_path): top_repo_path: str Top Git repository path file_path: str - The path of the file in .gitignore + The path of the file in .gitignore """ try: res = await self.ensure_gitignore(top_repo_path) @@ -1229,7 +1292,7 @@ async def version(self): if code == 0: version = GIT_VERSION_REGEX.match(output) if version is not None: - return version.group('version') + return version.group("version") return None @@ -1240,7 +1303,9 @@ async def tags(self, current_path): Git path repository """ command = ["git", "tag", "--list"] - code, output, error = await execute(command, cwd=os.path.join(self.root_dir, current_path)) + code, output, error = await execute( + command, cwd=os.path.join(self.root_dir, current_path) + ) if code != 0: return {"code": code, "command": " ".join(command), "message": error} tags = [tag for tag in output.split("\n") if len(tag) > 0] @@ -1255,7 +1320,9 @@ async def tag_checkout(self, current_path, tag): Tag to checkout """ command = ["git", "checkout", "tags/" + tag] - code, _, error = await execute(command, cwd=os.path.join(self.root_dir, current_path)) + code, _, error = await execute( + command, cwd=os.path.join(self.root_dir, current_path) + ) if code == 0: return {"code": code, "message": "Tag {} checked out".format(tag)} else: diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 8f3472379..c10b1ea5f 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -15,7 +15,7 @@ # Git configuration options exposed through the REST API -ALLOWED_OPTIONS = ['user.name', 'user.email'] +ALLOWED_OPTIONS = ["user.name", "user.email"] class GitHandler(APIHandler): @@ -278,7 +278,7 @@ async def post(self): name = data.get("name", DEFAULT_REMOTE_NAME) url = data["url"] output = await self.git.remote_add(top_repo_path, url, name) - if(output["code"] == 0): + if output["code"] == 0: self.set_status(201) else: self.set_status(500) @@ -410,7 +410,7 @@ async def post(self): current_path = self.get_json_body()["current_path"] current_branch = await self.git.get_current_branch(current_path) response = await self.git.get_upstream_branch(current_path, current_branch) - if response['code'] != 0: + if response["code"] != 0: self.set_status(500) self.finish(json.dumps(response)) @@ -426,7 +426,11 @@ async def post(self): POST request handler, pulls files from a remote branch to your current branch. """ data = self.get_json_body() - response = await self.git.pull(data["current_path"], data.get("auth", None), data.get("cancel_on_conflict", False)) + response = await self.git.pull( + data["current_path"], + data.get("auth", None), + data.get("cancel_on_conflict", False), + ) if response["code"] != 0: self.set_status(500) @@ -445,8 +449,8 @@ async def post(self): """ POST request handler, pushes committed files from your current branch to a remote branch - - Request body: + + Request body: { current_path: string, # Git repository path remote?: string # Remote to push to; i.e. or / @@ -457,27 +461,31 @@ async def post(self): known_remote = data.get("remote") current_local_branch = await self.git.get_current_branch(current_path) - + set_upstream = False current_upstream_branch = await self.git.get_upstream_branch( current_path, current_local_branch ) if known_remote is not None: - set_upstream = current_upstream_branch['code'] != 0 + set_upstream = current_upstream_branch["code"] != 0 remote_name, _, remote_branch = known_remote.partition("/") - + current_upstream_branch = { "code": 0, "remote_branch": remote_branch or current_local_branch, - "remote_short_name": remote_name + "remote_short_name": remote_name, } - if current_upstream_branch['code'] == 0: - branch = ":".join(["HEAD", current_upstream_branch['remote_branch']]) + if current_upstream_branch["code"] == 0: + branch = ":".join(["HEAD", current_upstream_branch["remote_branch"]]) response = await self.git.push( - current_upstream_branch['remote_short_name'], branch, current_path, data.get("auth", None), set_upstream + current_upstream_branch["remote_short_name"], + branch, + current_path, + data.get("auth", None), + set_upstream, ) else: @@ -488,7 +496,7 @@ async def post(self): config_options = config["options"] list_remotes = await self.git.remote_show(current_path) remotes = list_remotes.get("remotes", list()) - push_default = config_options.get('remote.pushdefault') + push_default = config_options.get("remote.pushdefault") default_remote = None if push_default is not None and push_default in remotes: @@ -498,10 +506,10 @@ async def post(self): if default_remote is not None: response = await self.git.push( - default_remote, - current_local_branch, - current_path, - data.get("auth", None), + default_remote, + current_local_branch, + current_path, + data.get("auth", None), set_upstream=True, ) else: @@ -510,7 +518,7 @@ async def post(self): "message": "fatal: The current branch {} has no upstream branch.".format( current_local_branch ), - "remotes": remotes # Returns the list of known remotes + "remotes": remotes, # Returns the list of known remotes } if response["code"] != 0: @@ -557,12 +565,14 @@ async def post(self): """ data = self.get_json_body() top_repo_path = data["path"] - options = data.get("options", {}) - + options = data.get("options", {}) + filtered_options = {k: v for k, v in options.items() if k in ALLOWED_OPTIONS} response = await self.git.config(top_repo_path, **filtered_options) if "options" in response: - response["options"] = {k:v for k, v in response["options"].items() if k in ALLOWED_OPTIONS} + response["options"] = { + k: v for k, v in response["options"].items() if k in ALLOWED_OPTIONS + } if response["code"] != 0: self.set_status(500) @@ -629,7 +639,9 @@ async def get(self): try: git_version = await self.git.version() except Exception as error: - self.log.debug("[jupyterlab_git] Failed to execute 'git' command: {!s}".format(error)) + self.log.debug( + "[jupyterlab_git] Failed to execute 'git' command: {!s}".format(error) + ) server_version = str(parse(__version__)) # Similar to https://github.com/jupyter/nbdime/blob/master/nbdime/webapp/nb_server_extension.py#L90-L91 root_dir = getattr(self.contents_manager, "root_dir", None) @@ -648,7 +660,7 @@ async def get(self): class GitTagHandler(GitHandler): """ - Handler for 'git tag '. Fetches list of all tags in current repository + Handler for 'git tag '. Fetches list of all tags in current repository """ @web.authenticated @@ -666,7 +678,7 @@ async def post(self): class GitTagCheckoutHandler(GitHandler): """ - Handler for 'git tag checkout '. Checkout the tag version of repo + Handler for 'git tag checkout '. Checkout the tag version of repo """ @web.authenticated @@ -687,7 +699,6 @@ async def post(self): # FIXME remove for 0.22 release - this avoid error when upgrading from 0.20 to 0.21 if the frontend # has not been rebuilt yet. class GitServerRootHandler(GitHandler): - @web.authenticated async def get(self): # Similar to https://github.com/jupyter/nbdime/blob/master/nbdime/webapp/nb_server_extension.py#L90-L91 @@ -732,7 +743,7 @@ def setup_handlers(web_app): ("/git/upstream", GitUpstreamHandler), ("/git/ignore", GitIgnoreHandler), ("/git/tags", GitTagHandler), - ("/git/tag_checkout", GitTagCheckoutHandler) + ("/git/tag_checkout", GitTagCheckoutHandler), ] # add the baseurl to our paths diff --git a/jupyterlab_git/log.py b/jupyterlab_git/log.py index a82333cbd..dc17a01da 100644 --- a/jupyterlab_git/log.py +++ b/jupyterlab_git/log.py @@ -11,7 +11,7 @@ def get_logger(cls) -> logging.Logger: if cls._LOGGER is None: app = Application.instance() cls._LOGGER = logging.getLogger("{!s}.jupyterlab_git".format(app.log.name)) - + return cls._LOGGER diff --git a/jupyterlab_git/tests/test_branch.py b/jupyterlab_git/tests/test_branch.py index 05ae62512..3b6be622c 100644 --- a/jupyterlab_git/tests/test_branch.py +++ b/jupyterlab_git/tests/test_branch.py @@ -44,7 +44,8 @@ async def test_get_current_branch_success(): # Then mock_execute.assert_called_once_with( - ["git", "symbolic-ref", "--short", "HEAD"], cwd=os.path.join("/bin", "test_curr_path") + ["git", "symbolic-ref", "--short", "HEAD"], + cwd=os.path.join("/bin", "test_curr_path"), ) assert "feature-foo" == actual_response @@ -87,8 +88,8 @@ async def test_checkout_branch_noref_failure(): branch = "test-branch" curr_path = "test_curr_path" stdout_message = "" - stderr_message = "error: pathspec '{}' did not match any file(s) known to git".format( - branch + stderr_message = ( + "error: pathspec '{}' did not match any file(s) known to git".format(branch) ) rc = 1 with patch("jupyterlab_git.git.execute") as mock_execute: @@ -160,8 +161,8 @@ async def test_checkout_branch_headsref_failure(): branch = "test-branch" curr_path = "test_curr_path" stdout_message = "" - stderr_message = "error: pathspec '{}' did not match any file(s) known to git".format( - branch + stderr_message = ( + "error: pathspec '{}' did not match any file(s) known to git".format(branch) ) rc = 1 @@ -230,8 +231,8 @@ async def test_checkout_branch_headsref_success(): async def test_checkout_branch_remoteref_failure(): branch = "test-branch" stdout_message = "" - stderr_message = "error: pathspec '{}' did not match any file(s) known to git".format( - branch + stderr_message = ( + "error: pathspec '{}' did not match any file(s) known to git".format(branch) ) rc = 1 @@ -337,7 +338,8 @@ async def test_get_current_branch_failure(): # Then mock_execute.assert_called_once_with( - ["git", "symbolic-ref", "--short", "HEAD"], cwd=os.path.join("/bin", "test_curr_path") + ["git", "symbolic-ref", "--short", "HEAD"], + cwd=os.path.join("/bin", "test_curr_path"), ) assert ( "Error [fatal: Not a git repository (or any of the parent directories): .git] " @@ -356,9 +358,7 @@ async def test_get_current_branch_detached_success(): " remotes/origin/feature-foo", " remotes/origin/HEAD", ] - mock_execute.return_value = maybe_future( - (0, "\n".join(process_output), "") - ) + mock_execute.return_value = maybe_future((0, "\n".join(process_output), "")) # When actual_response = await Git( @@ -414,9 +414,9 @@ async def test_get_upstream_branch_success(branch, upstream, remotename): with patch("jupyterlab_git.git.execute") as mock_execute: # Given mock_execute.side_effect = [ - maybe_future((0, remotename + '/' + upstream, '')), - maybe_future((0, remotename, '')) - ] + maybe_future((0, remotename + "/" + upstream, "")), + maybe_future((0, remotename, "")), + ] # When actual_response = await Git(FakeContentManager("/bin")).get_upstream_branch( @@ -427,18 +427,26 @@ async def test_get_upstream_branch_success(branch, upstream, remotename): mock_execute.assert_has_calls( [ call( - ["git", "rev-parse", "--abbrev-ref", "{}@{{upstream}}".format(branch)], + [ + "git", + "rev-parse", + "--abbrev-ref", + "{}@{{upstream}}".format(branch), + ], cwd=os.path.join("/bin", "test_curr_path"), ), call( - ['git', 'config', '--local', 'branch.{}.remote'.format(branch)], - cwd='/bin/test_curr_path', + ["git", "config", "--local", "branch.{}.remote".format(branch)], + cwd="/bin/test_curr_path", ), - ], any_order=False, ) - assert {'code': 0, 'remote_branch': upstream, 'remote_short_name': remotename} == actual_response + assert { + "code": 0, + "remote_branch": upstream, + "remote_short_name": remotename, + } == actual_response @pytest.mark.asyncio @@ -470,7 +478,11 @@ async def test_get_upstream_branch_failure(outputs, message): response = await Git(FakeContentManager("/bin")).get_upstream_branch( current_path="test_curr_path", branch_name="blah" ) - expected = {'code': 128, 'command': 'git rev-parse --abbrev-ref blah@{upstream}', 'message': outputs[2]} + expected = { + "code": 128, + "command": "git rev-parse --abbrev-ref blah@{upstream}", + "message": outputs[2], + } assert response == expected @@ -748,9 +760,7 @@ async def test_branch_success_detached_head(): # Response for get all refs/heads maybe_future((0, "\n".join(process_output_heads), "")), # Response for get current branch - maybe_future( - (128, "", "fatal: ref HEAD is not a symbolic ref") - ), + maybe_future((128, "", "fatal: ref HEAD is not a symbolic ref")), # Response for get current branch detached maybe_future((0, "\n".join(detached_head_output), "")), # Response for get all refs/remotes diff --git a/jupyterlab_git/tests/test_clone.py b/jupyterlab_git/tests/test_clone.py index 42a6bdd3b..3fc78d481 100644 --- a/jupyterlab_git/tests/test_clone.py +++ b/jupyterlab_git/tests/test_clone.py @@ -154,4 +154,4 @@ async def test_git_clone_with_auth_auth_failure_from_git(): assert { "code": 128, "message": "remote: Invalid username or password.\r\nfatal: Authentication failed for 'ghjkhjkl'", - } == actual_response \ No newline at end of file + } == actual_response diff --git a/jupyterlab_git/tests/test_detailed_log.py b/jupyterlab_git/tests/test_detailed_log.py index 0984059bb..3f6292021 100644 --- a/jupyterlab_git/tests/test_detailed_log.py +++ b/jupyterlab_git/tests/test_detailed_log.py @@ -28,9 +28,8 @@ async def test_detailed_log(): "-\t-\tbinary_file.png", ] - mock_execute.return_value = maybe_future( - (0, "\x00".join(process_output)+"\x00", "") + (0, "\x00".join(process_output) + "\x00", "") ) expected_response = { @@ -93,12 +92,9 @@ async def test_detailed_log(): } # When - actual_response = (await - Git(FakeContentManager("/bin")) - .detailed_log( - selected_hash="f29660a2472e24164906af8653babeb48e4bf2ab", - current_path="test_curr_path", - ) + actual_response = await Git(FakeContentManager("/bin")).detailed_log( + selected_hash="f29660a2472e24164906af8653babeb48e4bf2ab", + current_path="test_curr_path", ) # Then diff --git a/jupyterlab_git/tests/test_diff.py b/jupyterlab_git/tests/test_diff.py index f9f5da5c8..98bf443b0 100644 --- a/jupyterlab_git/tests/test_diff.py +++ b/jupyterlab_git/tests/test_diff.py @@ -23,13 +23,12 @@ async def test_changed_files_single_commit(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given - mock_execute.return_value = maybe_future( - (0, "file1.ipynb\x00file2.py\x00", "") - ) + mock_execute.return_value = maybe_future((0, "file1.ipynb\x00file2.py\x00", "")) # When actual_response = await Git(FakeContentManager("/bin")).changed_files( - current_path="test-path", single_commit="64950a634cd11d1a01ddfedaeffed67b531cb11e^!" + current_path="test-path", + single_commit="64950a634cd11d1a01ddfedaeffed67b531cb11e^!", ) # Then @@ -50,9 +49,7 @@ async def test_changed_files_single_commit(): async def test_changed_files_working_tree(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given - mock_execute.return_value = maybe_future( - (0, "file1.ipynb\x00file2.py", "") - ) + mock_execute.return_value = maybe_future((0, "file1.ipynb\x00file2.py", "")) # When actual_response = await Git(FakeContentManager("/bin")).changed_files( @@ -70,9 +67,7 @@ async def test_changed_files_working_tree(): async def test_changed_files_index(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given - mock_execute.return_value = maybe_future( - (0, "file1.ipynb\x00file2.py", "") - ) + mock_execute.return_value = maybe_future((0, "file1.ipynb\x00file2.py", "")) # When actual_response = await Git(FakeContentManager("/bin")).changed_files( @@ -81,7 +76,8 @@ async def test_changed_files_index(): # Then mock_execute.assert_called_once_with( - ["git", "diff", "--staged", "HEAD", "--name-only", "-z"], cwd="/bin/test-path" + ["git", "diff", "--staged", "HEAD", "--name-only", "-z"], + cwd="/bin/test-path", ) assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response @@ -90,18 +86,17 @@ async def test_changed_files_index(): async def test_changed_files_two_commits(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given - mock_execute.return_value = maybe_future( - (0, "file1.ipynb\x00file2.py", "") - ) + mock_execute.return_value = maybe_future((0, "file1.ipynb\x00file2.py", "")) # When actual_response = await Git(FakeContentManager("/bin")).changed_files( - current_path = "test-path", base="HEAD", remote="origin/HEAD" + current_path="test-path", base="HEAD", remote="origin/HEAD" ) # Then mock_execute.assert_called_once_with( - ["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], cwd="/bin/test-path" + ["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], + cwd="/bin/test-path", ) assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response @@ -119,50 +114,106 @@ async def test_changed_files_git_diff_error(): # Then mock_execute.assert_called_once_with( - ["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], cwd="/bin/test-path" + ["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], + cwd="/bin/test-path", ) assert {"code": 128, "message": "error message"} == actual_response @pytest.mark.asyncio -@pytest.mark.parametrize("args, cli_result, cmd, expected", [ - ( - ("dummy.txt", "ar539ie5", "/bin"), - (0, "2\t1\tdummy.txt", ""), - ["git", "diff", "--numstat", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", "ar539ie5", "--", "dummy.txt"], - False - ), - ( - ("dummy.png", "ar539ie5", "/bin"), - (0, "-\t-\tdummy.png", ""), - ["git", "diff", "--numstat", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", "ar539ie5", "--", "dummy.png"], - True - ), - ( - ("dummy.txt", "INDEX", "/bin"), - (0, "2\t1\tdummy.txt", ""), - ["git", "diff", "--numstat", "--cached", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", "--", "dummy.txt"], - False - ), - ( - ("dummy.png", "INDEX", "/bin"), - (0, "-\t-\tdummy.png", ""), - ["git", "diff", "--numstat", "--cached", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", "--", "dummy.png"], - True - ), - ( - ("dummy.txt", "ar539ie5", "/bin"), - (128, "", "fatal: Git command failed"), - ["git", "diff", "--numstat", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", "ar539ie5", "--", "dummy.txt"], - tornado.web.HTTPError - ), - ( - ("dummy.txt", "ar539ie5", "/bin"), - (128, "", "fatal: Path 'dummy.txt' does not exist (neither on disk nor in the index)"), - ["git", "diff", "--numstat", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", "ar539ie5", "--", "dummy.txt"], - False - ), -]) +@pytest.mark.parametrize( + "args, cli_result, cmd, expected", + [ + ( + ("dummy.txt", "ar539ie5", "/bin"), + (0, "2\t1\tdummy.txt", ""), + [ + "git", + "diff", + "--numstat", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "ar539ie5", + "--", + "dummy.txt", + ], + False, + ), + ( + ("dummy.png", "ar539ie5", "/bin"), + (0, "-\t-\tdummy.png", ""), + [ + "git", + "diff", + "--numstat", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "ar539ie5", + "--", + "dummy.png", + ], + True, + ), + ( + ("dummy.txt", "INDEX", "/bin"), + (0, "2\t1\tdummy.txt", ""), + [ + "git", + "diff", + "--numstat", + "--cached", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "--", + "dummy.txt", + ], + False, + ), + ( + ("dummy.png", "INDEX", "/bin"), + (0, "-\t-\tdummy.png", ""), + [ + "git", + "diff", + "--numstat", + "--cached", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "--", + "dummy.png", + ], + True, + ), + ( + ("dummy.txt", "ar539ie5", "/bin"), + (128, "", "fatal: Git command failed"), + [ + "git", + "diff", + "--numstat", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "ar539ie5", + "--", + "dummy.txt", + ], + tornado.web.HTTPError, + ), + ( + ("dummy.txt", "ar539ie5", "/bin"), + ( + 128, + "", + "fatal: Path 'dummy.txt' does not exist (neither on disk nor in the index)", + ), + [ + "git", + "diff", + "--numstat", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "ar539ie5", + "--", + "dummy.txt", + ], + False, + ), + ], +) async def test_is_binary_file(args, cli_result, cmd, expected): with patch("jupyterlab_git.git.execute") as mock_execute: # Given diff --git a/jupyterlab_git/tests/test_execute.py b/jupyterlab_git/tests/test_execute.py index a60e17e90..0e6d99403 100644 --- a/jupyterlab_git/tests/test_execute.py +++ b/jupyterlab_git/tests/test_execute.py @@ -13,15 +13,15 @@ async def test_execute_waits_on_index_lock(tmp_path): async def remove_lock_file(*args): assert "unlocked" not in repr(execution_lock) # Check that the lock is working lock_file.unlink() # Raise an error for missing file - + with patch("tornado.gen.sleep") as sleep: - sleep.side_effect = remove_lock_file # Remove the lock file instead of sleeping - + sleep.side_effect = remove_lock_file # Remove the lock file instead of sleeping + assert "unlock" in repr(execution_lock) cmd = ["git", "dummy"] kwargs = {"cwd": "{!s}".format(tmp_path)} await execute(cmd, **kwargs) assert "unlock" in repr(execution_lock) - + assert not lock_file.exists() assert sleep.call_count == 1 diff --git a/jupyterlab_git/tests/test_handlers.py b/jupyterlab_git/tests/test_handlers.py index 10e836f41..67ebd1426 100644 --- a/jupyterlab_git/tests/test_handlers.py +++ b/jupyterlab_git/tests/test_handlers.py @@ -169,11 +169,7 @@ def test_push_handler_localbranch(self, mock_git): # Given mock_git.get_current_branch.return_value = maybe_future("localbranch") mock_git.get_upstream_branch.return_value = maybe_future( - { - "code": 0, - "remote_short_name": ".", - "remote_branch": "localbranch" - } + {"code": 0, "remote_short_name": ".", "remote_branch": "localbranch"} ) mock_git.push.return_value = maybe_future({"code": 0}) @@ -184,7 +180,9 @@ def test_push_handler_localbranch(self, mock_git): # Then mock_git.get_current_branch.assert_called_with("test_path") mock_git.get_upstream_branch.assert_called_with("test_path", "localbranch") - mock_git.push.assert_called_with(".", "HEAD:localbranch", "test_path", None, False) + mock_git.push.assert_called_with( + ".", "HEAD:localbranch", "test_path", None, False + ) assert response.status_code == 200 payload = response.json() @@ -194,9 +192,11 @@ def test_push_handler_localbranch(self, mock_git): def test_push_handler_remotebranch(self, mock_git): # Given mock_git.get_current_branch.return_value = maybe_future("foo/bar") - upstream = {"code": 0, - "remote_short_name": "origin/something", - "remote_branch": "remote-branch-name"} + upstream = { + "code": 0, + "remote_short_name": "origin/something", + "remote_branch": "remote-branch-name", + } mock_git.get_upstream_branch.return_value = maybe_future(upstream) mock_git.push.return_value = maybe_future({"code": 0}) @@ -222,7 +222,7 @@ def test_push_handler_noupstream(self, mock_git): upstream = { "code": 128, "command": "", - "message": "fatal: no upstream configured for branch 'foo'" + "message": "fatal: no upstream configured for branch 'foo'", } mock_git.get_upstream_branch.return_value = maybe_future(upstream) mock_git.config.return_value = maybe_future({"options": dict()}) @@ -250,7 +250,7 @@ def test_push_handler_noupstream(self, mock_git): assert payload == { "code": 128, "message": "fatal: The current branch foo has no upstream branch.", - "remotes": list() + "remotes": list(), } @patch("jupyterlab_git.handlers.GitPushHandler.git", spec=Git) @@ -285,7 +285,7 @@ def test_push_handler_multipleupstream(self, mock_git): assert payload == { "code": 128, "message": "fatal: The current branch foo has no upstream branch.", - "remotes": remotes + "remotes": remotes, } @patch("jupyterlab_git.handlers.GitPushHandler.git", spec=Git) @@ -310,7 +310,9 @@ def test_push_handler_noupstream_unique_remote(self, mock_git): mock_git.get_upstream_branch.assert_called_with(path, "foo") mock_git.config.assert_called_with(path) mock_git.remote_show.assert_called_with(path) - mock_git.push.assert_called_with(remote, "foo", "test_path", None, set_upstream=True) + mock_git.push.assert_called_with( + remote, "foo", "test_path", None, set_upstream=True + ) assert response.status_code == 200 payload = response.json() @@ -323,8 +325,12 @@ def test_push_handler_noupstream_pushdefault(self, mock_git): mock_git.get_current_branch.return_value = maybe_future("foo") upstream = {"code": -1, "message": "oups"} mock_git.get_upstream_branch.return_value = maybe_future(upstream) - mock_git.config.return_value = maybe_future({"options": {"remote.pushdefault": remote}}) - mock_git.remote_show.return_value = maybe_future({"remotes": [remote, "upstream"]}) + mock_git.config.return_value = maybe_future( + {"options": {"remote.pushdefault": remote}} + ) + mock_git.remote_show.return_value = maybe_future( + {"remotes": [remote, "upstream"]} + ) mock_git.push.return_value = maybe_future({"code": 0}) path = "test_path" @@ -338,7 +344,9 @@ def test_push_handler_noupstream_pushdefault(self, mock_git): mock_git.get_upstream_branch.assert_called_with(path, "foo") mock_git.config.assert_called_with(path) mock_git.remote_show.assert_called_with(path) - mock_git.push.assert_called_with(remote, "foo", "test_path", None, set_upstream=True) + mock_git.push.assert_called_with( + remote, "foo", "test_path", None, set_upstream=True + ) assert response.status_code == 200 payload = response.json() @@ -409,9 +417,11 @@ class TestUpstream(ServerTest): def test_upstream_handler_forward_slashes(self, mock_git): # Given mock_git.get_current_branch.return_value = maybe_future("foo/bar") - upstream = {"code": 0, - "remote_short_name": "origin/something", - "remote_branch": "foo/bar"} + upstream = { + "code": 0, + "remote_short_name": "origin/something", + "remote_branch": "foo/bar", + } mock_git.get_upstream_branch.return_value = maybe_future(upstream) # When @@ -424,15 +434,13 @@ def test_upstream_handler_forward_slashes(self, mock_git): assert response.status_code == 200 payload = response.json() - assert payload == upstream + assert payload == upstream @patch("jupyterlab_git.handlers.GitUpstreamHandler.git", spec=Git) def test_upstream_handler_localbranch(self, mock_git): # Given mock_git.get_current_branch.return_value = maybe_future("foo/bar") - upstream = {"code": 0, - "remote_short_name": ".", - "remote_branch": "foo/bar"} + upstream = {"code": 0, "remote_short_name": ".", "remote_branch": "foo/bar"} mock_git.get_upstream_branch.return_value = maybe_future(upstream) # When @@ -445,7 +453,7 @@ def test_upstream_handler_localbranch(self, mock_git): assert response.status_code == 200 payload = response.json() - assert payload == upstream + assert payload == upstream class TestDiffContent(ServerTest): @@ -460,7 +468,7 @@ def test_diffcontent(self, mock_execute): maybe_future((0, "1\t1\t{}".format(filename), "")), maybe_future((0, content, "")), maybe_future((0, "1\t1\t{}".format(filename), "")), - maybe_future((0, content, "")) + maybe_future((0, content, "")), ] # When @@ -486,9 +494,9 @@ def test_diffcontent(self, mock_execute): call( ["git", "show", "{}:{}".format("current", filename)], cwd=os.path.join(self.notebook_dir, top_repo_path), - ) + ), ], - any_order=True + any_order=True, ) @patch("jupyterlab_git.git.execute") @@ -501,7 +509,7 @@ def test_diffcontent_working(self, mock_execute): mock_execute.side_effect = [ maybe_future((0, "1\t1\t{}".format(filename), "")), maybe_future((0, content, "")), - maybe_future((0, content, "")) + maybe_future((0, content, "")), ] dummy_file = os.path.join(self.notebook_dir, top_repo_path, filename) @@ -543,7 +551,7 @@ def test_diffcontent_index(self, mock_execute): maybe_future((0, "1\t1\t{}".format(filename), "")), maybe_future((0, content, "")), maybe_future((0, "1\t1\t{}".format(filename), "")), - maybe_future((0, content, "")) + maybe_future((0, content, "")), ] # When @@ -569,9 +577,9 @@ def test_diffcontent_index(self, mock_execute): call( ["git", "show", "{}:{}".format("", filename)], cwd=os.path.join(self.notebook_dir, top_repo_path), - ) + ), ], - any_order=True + any_order=True, ) @patch("jupyterlab_git.git.execute") @@ -585,7 +593,7 @@ def test_diffcontent_unknown_special(self, mock_execute): maybe_future((0, "1\t1\t{}".format(filename), "")), maybe_future((0, content, "")), maybe_future((0, "1\t1\t{}".format(filename), "")), - maybe_future((0, content, "")) + maybe_future((0, content, "")), ] # When @@ -645,7 +653,7 @@ def test_diffcontent_binary(self, mock_execute): "curr_ref": {"git": "current"}, "top_repo_path": top_repo_path, } - + # Then with assert_http_error(500, msg="file is not UTF-8"): self.tester.post(["diffcontent"], body=body) @@ -680,7 +688,7 @@ def test_diffcontent_getcontent_error(self, mock_execute): mock_execute.side_effect = [ maybe_future((0, "1\t1\t{}".format(filename), "")), maybe_future((0, content, "")), - maybe_future((0, content, "")) + maybe_future((0, content, "")), ] # When diff --git a/jupyterlab_git/tests/test_pushpull.py b/jupyterlab_git/tests/test_pushpull.py index d8628bb78..237cf81fa 100644 --- a/jupyterlab_git/tests/test_pushpull.py +++ b/jupyterlab_git/tests/test_pushpull.py @@ -37,20 +37,33 @@ async def test_git_pull_with_conflict_fail(): with patch("os.environ", {"TEST": "test"}): with patch("jupyterlab_git.git.execute") as mock_execute: # Given - mock_execute.return_value = maybe_future((1, "", "Automatic merge failed; fix conflicts and then commit the result.")) + mock_execute.return_value = maybe_future( + ( + 1, + "", + "Automatic merge failed; fix conflicts and then commit the result.", + ) + ) # When - actual_response = await Git(FakeContentManager("/bin")).pull("test_curr_path") + actual_response = await Git(FakeContentManager("/bin")).pull( + "test_curr_path" + ) # Then - mock_execute.assert_has_calls([ - call( - ["git", "pull", "--no-commit"], - cwd="/bin/test_curr_path", - env={"TEST": "test", "GIT_TERMINAL_PROMPT": "0"}, - ) - ]) - assert {"code": 1, "message": "Automatic merge failed; fix conflicts and then commit the result."} == actual_response + mock_execute.assert_has_calls( + [ + call( + ["git", "pull", "--no-commit"], + cwd="/bin/test_curr_path", + env={"TEST": "test", "GIT_TERMINAL_PROMPT": "0"}, + ) + ] + ) + assert { + "code": 1, + "message": "Automatic merge failed; fix conflicts and then commit the result.", + } == actual_response @pytest.mark.asyncio @@ -140,26 +153,36 @@ async def test_git_pull_with_auth_success_and_conflict_fail(): with patch("os.environ", {"TEST": "test"}): with patch("jupyterlab_git.git.execute") as mock_execute_with_authentication: # Given - mock_execute_with_authentication.return_value = maybe_future((1, "output", "Automatic merge failed; fix conflicts and then commit the result.")) + mock_execute_with_authentication.return_value = maybe_future( + ( + 1, + "output", + "Automatic merge failed; fix conflicts and then commit the result.", + ) + ) # When - auth = { - "username" : "asdf", - "password" : "qwerty" - } - actual_response = await Git(FakeContentManager("/bin")).pull("test_curr_path", auth) + auth = {"username": "asdf", "password": "qwerty"} + actual_response = await Git(FakeContentManager("/bin")).pull( + "test_curr_path", auth + ) # Then - mock_execute_with_authentication.assert_has_calls([ - call( - ["git", "pull", "--no-commit"], - cwd="/bin/test_curr_path", - env={"TEST": "test", "GIT_TERMINAL_PROMPT": "1"}, - username="asdf", - password="qwerty" - ) - ]) - assert {"code": 1, "message": "Automatic merge failed; fix conflicts and then commit the result."} == actual_response + mock_execute_with_authentication.assert_has_calls( + [ + call( + ["git", "pull", "--no-commit"], + cwd="/bin/test_curr_path", + env={"TEST": "test", "GIT_TERMINAL_PROMPT": "1"}, + username="asdf", + password="qwerty", + ) + ] + ) + assert { + "code": 1, + "message": "Automatic merge failed; fix conflicts and then commit the result.", + } == actual_response @pytest.mark.asyncio @@ -225,9 +248,7 @@ async def test_git_push_success(): with patch("os.environ", {"TEST": "test"}): with patch("jupyterlab_git.git.execute") as mock_execute: # Given - mock_execute.return_value = maybe_future( - (0, "output", "does not matter") - ) + mock_execute.return_value = maybe_future((0, "output", "does not matter")) # When actual_response = await Git(FakeContentManager("/bin")).push( diff --git a/jupyterlab_git/tests/test_settings.py b/jupyterlab_git/tests/test_settings.py index 191b1ce65..49172d140 100644 --- a/jupyterlab_git/tests/test_settings.py +++ b/jupyterlab_git/tests/test_settings.py @@ -33,12 +33,14 @@ def test_git_get_settings_success(self, mock_execute): "serverRoot": self.notebook.notebook_dir, "serverVersion": str(parse(__version__)), } - + @patch("jupyterlab_git.git.execute") def test_git_get_settings_no_git(self, mock_execute): # Given jlab_version = "2.1.42-alpha.24" - mock_execute.side_effect = FileNotFoundError("[Errno 2] No such file or directory: 'git'") + mock_execute.side_effect = FileNotFoundError( + "[Errno 2] No such file or directory: 'git'" + ) # When response = self.tester.get(["settings"], params={"version": jlab_version}) diff --git a/jupyterlab_git/tests/test_tag.py b/jupyterlab_git/tests/test_tag.py index ce64e0a7d..ec19e0271 100644 --- a/jupyterlab_git/tests/test_tag.py +++ b/jupyterlab_git/tests/test_tag.py @@ -8,6 +8,7 @@ from .testutils import FakeContentManager, ServerTest, maybe_future + @pytest.mark.asyncio async def test_git_tag_success(): with patch("jupyterlab_git.git.execute") as mock_execute: @@ -26,6 +27,7 @@ async def test_git_tag_success(): assert {"code": 0, "tags": [tag]} == actual_response + @pytest.mark.asyncio async def test_git_tag_checkout_success(): with patch("os.environ", {"TEST": "test"}): @@ -35,7 +37,9 @@ async def test_git_tag_checkout_success(): mock_execute.return_value = maybe_future((0, "", "")) # When - actual_response = await Git(FakeContentManager("/bin")).tag_checkout("test_curr_path", "mock_tag") + actual_response = await Git(FakeContentManager("/bin")).tag_checkout( + "test_curr_path", "mock_tag" + ) # Then mock_execute.assert_called_once_with( @@ -43,4 +47,7 @@ async def test_git_tag_checkout_success(): cwd=os.path.join("/bin", "test_curr_path"), ) - assert {"code": 0, "message": "Tag {} checked out".format(tag)} == actual_response + assert { + "code": 0, + "message": "Tag {} checked out".format(tag), + } == actual_response diff --git a/jupyterlab_git/tests/testutils.py b/jupyterlab_git/tests/testutils.py index d5df20f03..910f3e9e0 100644 --- a/jupyterlab_git/tests/testutils.py +++ b/jupyterlab_git/tests/testutils.py @@ -83,10 +83,9 @@ def setUp(self): class FakeContentManager: - def __init__(self, root_dir): self.root_dir = root_dir - + def get(self, path=None): return {"content": ""} diff --git a/package.json b/package.json index d9e68815e..a0f15cacc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jupyterlab/git", - "version": "0.21.1", + "version": "0.22.0-alpha.0", "description": "A JupyterLab extension for version control using git", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/release.py b/release.py index 7612bc484..173757db7 100755 --- a/release.py +++ b/release.py @@ -6,65 +6,85 @@ from setupbase import get_version -VERSION_PY = 'jupyterlab_git/_version.py' +VERSION_PY = "jupyterlab_git/_version.py" + def assertEqualVersion(): serverVersion = parse(serverExtensionVersion()) frontendVersion = parse(labExtensionVersion()) - error_msg = "Frontend ({}) and server ({}) version do not match".format(frontendVersion, serverVersion) + error_msg = "Frontend ({}) and server ({}) version do not match".format( + frontendVersion, serverVersion + ) assert serverVersion == frontendVersion, error_msg + def prepLabextensionBundle(): - subprocess.run(['jlpm', 'clean:slate']) + subprocess.run(["jlpm", "clean:slate"]) + def tag(version, dryrun=False, kind=None): - """git tagging - """ - kw = {'version': version, 'kind': kind} - tag = '{kind}_v{version}'.format(**kw) if kind else 'v{version}'.format(**kw) + """git tagging""" + kw = {"version": version, "kind": kind} + tag = "{kind}_v{version}".format(**kw) if kind else "v{version}".format(**kw) if dryrun: print("Would tag: {}".format(tag)) else: - subprocess.run(['git', 'tag', tag]) - subprocess.run(['git', 'push', 'upstream', tag]) + subprocess.run(["git", "tag", tag]) + subprocess.run(["git", "push", "upstream", tag]) + def pypi(wheel=True, test=False): - """release on pypi - """ + """release on pypi""" if wheel: # build the source (sdist) and binary wheel (bdist_wheel) releases - subprocess.run(['python', 'setup.py', 'sdist', 'bdist_wheel']) + subprocess.run(["python", "setup.py", "sdist", "bdist_wheel"]) else: # build just the source release - subprocess.run(['python', 'setup.py', 'sdist']) + subprocess.run(["python", "setup.py", "sdist"]) if test: # release to the test server - subprocess.run(['twine', 'upload', '--repository-url', 'https://test.pypi.org/legacy/', 'dist/*']) + subprocess.run( + [ + "twine", + "upload", + "--repository-url", + "https://test.pypi.org/legacy/", + "dist/*", + ] + ) else: # release to the production server - subprocess.run(['twine', 'upload', 'dist/*']) + subprocess.run(["twine", "upload", "dist/*"]) + def npmjs(dryrun=False): - """release on npmjs - """ + """release on npmjs""" if dryrun: # dry run build and release - subprocess.run(['npm', 'publish', '--access', 'public', '--dry-run']) + subprocess.run(["npm", "publish", "--access", "public", "--dry-run"]) else: # build and release - subprocess.run(['npm', 'publish', '--access', 'public']) + subprocess.run(["npm", "publish", "--access", "public"]) + def labExtensionVersion(dryrun=False, version=None): if version: - if 'rc' in version: - version,rc = version.split('rc') - version = version + '-rc.{}'.format(rc) - - force_ver_cmd = ['npm', '--no-git-tag-version', 'version', version, '--force', '--allow-same-version'] - force_ver_info = ' '.join(force_ver_cmd) + if "rc" in version: + version, rc = version.split("rc") + version = version + "-rc.{}".format(rc) + + force_ver_cmd = [ + "npm", + "--no-git-tag-version", + "version", + version, + "--force", + "--allow-same-version", + ] + force_ver_info = " ".join(force_ver_cmd) if dryrun: print("Would force npm version with: {}".format(force_ver_info)) @@ -74,17 +94,19 @@ def labExtensionVersion(dryrun=False, version=None): subprocess.run(force_ver_cmd) else: # get single source of truth from the Typescript labextension - with open('package.json') as f: + with open("package.json") as f: info = json.load(f) - version = info['version'] + version = info["version"] return version + def serverExtensionVersion(): # get single source of truth from the Python serverextension return get_version(VERSION_PY) + def doRelease(test=False): # treat the serverextension version as the "real" single source of truth version = serverExtensionVersion() @@ -101,15 +123,20 @@ def doRelease(test=False): pypi(test=test) npmjs(dryrun=test) + def main(): parser = argpar.ArgumentParser() - parser.add_argument('--test', action='store_true', - help='Release to Pypi test server; performs a dryrun of all other release actions') + parser.add_argument( + "--test", + action="store_true", + help="Release to Pypi test server; performs a dryrun of all other release actions", + ) parsed = vars(parser.parse_args()) - doRelease(test=parsed['test']) + doRelease(test=parsed["test"]) + -if __name__=='__main__': +if __name__ == "__main__": main() diff --git a/setup.py b/setup.py index 2e785dc46..3c2565c04 100644 --- a/setup.py +++ b/setup.py @@ -7,83 +7,91 @@ from subprocess import CalledProcessError from setupbase import ( - command_for_func, create_cmdclass, ensure_python, - get_version, HERE, run + command_for_func, + create_cmdclass, + ensure_python, + get_version, + HERE, + run, ) import setuptools # The name of the project -name='jupyterlab_git' +name = "jupyterlab_git" # Ensure a valid python version -ensure_python('>=3.5') +ensure_python(">=3.6") # Get our version -version = get_version(str(Path(name) / '_version.py')) +version = get_version(str(Path(name) / "_version.py")) -lab_path = Path(HERE) / name / 'labextension' +lab_path = Path(HERE) / name / "labextension" data_files_spec = [ - ('share/jupyter/lab/extensions', str(lab_path), '*.tgz'), - ('etc/jupyter/jupyter_notebook_config.d', - 'jupyter-config/jupyter_notebook_config.d', 'jupyterlab_git.json'), + ("share/jupyter/lab/extensions", str(lab_path), "*.tgz"), + ( + "etc/jupyter/jupyter_notebook_config.d", + "jupyter-config/jupyter_notebook_config.d", + "jupyterlab_git.json", + ), ] + def runPackLabextension(): - if Path('package.json').is_file(): + if Path("package.json").is_file(): try: - run(['jlpm', 'build:labextension']) + run(["jlpm", "build:labextension"]) except CalledProcessError: pass + + pack_labext = command_for_func(runPackLabextension) -cmdclass = create_cmdclass('pack_labext', data_files_spec=data_files_spec) -cmdclass['pack_labext'] = pack_labext -cmdclass.pop('develop') +cmdclass = create_cmdclass("pack_labext", data_files_spec=data_files_spec) +cmdclass["pack_labext"] = pack_labext +cmdclass.pop("develop") with open("README.md", "r") as fh: long_description = fh.read() setup_args = dict( - name = name, - description = "A server extension for JupyterLab's git extension", - long_description= long_description, + name=name, + description="A server extension for JupyterLab's git extension", + long_description=long_description, long_description_content_type="text/markdown", - version = version, - cmdclass = cmdclass, - packages = setuptools.find_packages(), - author = 'Jupyter Development Team', - url = 'https://github.com/jupyterlab/jupyterlab-git', - license = 'BSD', - platforms = "Linux, Mac OS X, Windows", - keywords = ['Jupyter', 'JupyterLab', 'Git'], - classifiers = [ - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Framework :: Jupyter', - ], - install_requires = [ - 'notebook', - 'nbdime ~=2.0', - 'packaging', - 'pexpect' + version=version, + cmdclass=cmdclass, + packages=setuptools.find_packages(), + author="Jupyter Development Team", + url="https://github.com/jupyterlab/jupyterlab-git", + license="BSD", + platforms="Linux, Mac OS X, Windows", + keywords=["Jupyter", "JupyterLab", "Git"], + classifiers=[ + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Framework :: Jupyter", ], - extras_require = { - 'test': [ - 'requests_unixsocket', - 'pytest', - 'pytest-asyncio', - 'jupyterlab~=2.0', + install_requires=["notebook", "nbdime ~=2.0", "packaging", "pexpect"], + extras_require={ + "test": [ + "requests_unixsocket", + "pytest", + "pytest-asyncio", + "jupyterlab~=2.0", + "black", + "pre-commit", ], }, + python_requires=">=3.6,<4", ) setuptools.setup(**setup_args) diff --git a/setupbase.py b/setupbase.py index 24c347916..1b918f2c9 100644 --- a/setupbase.py +++ b/setupbase.py @@ -20,7 +20,8 @@ # BEFORE importing distutils, remove MANIFEST. distutils doesn't properly # update it when the contents of directories change. -if os.path.exists('MANIFEST'): os.remove('MANIFEST') +if os.path.exists("MANIFEST"): + os.remove("MANIFEST") from distutils.cmd import Command @@ -36,29 +37,32 @@ except ImportError: bdist_wheel = None -if sys.platform == 'win32': +if sys.platform == "win32": from subprocess import list2cmdline else: + def list2cmdline(cmd_list): - return ' '.join(map(pipes.quote, cmd_list)) + return " ".join(map(pipes.quote, cmd_list)) -__version__ = '0.2.0' +__version__ = "0.2.0" # --------------------------------------------------------------------------- # Top Level Variables # --------------------------------------------------------------------------- HERE = os.path.abspath(os.path.dirname(__file__)) -is_repo = os.path.exists(pjoin(HERE, '.git')) -node_modules = pjoin(HERE, 'node_modules') +is_repo = os.path.exists(pjoin(HERE, ".git")) +node_modules = pjoin(HERE, "node_modules") SEPARATORS = os.sep if os.altsep is None else os.sep + os.altsep -npm_path = ':'.join([ - pjoin(HERE, 'node_modules', '.bin'), - os.environ.get('PATH', os.defpath), -]) +npm_path = ":".join( + [ + pjoin(HERE, "node_modules", ".bin"), + os.environ.get("PATH", os.defpath), + ] +) if "--skip-npm" in sys.argv: print("Skipping npm install as requested.") @@ -72,7 +76,8 @@ def list2cmdline(cmd_list): # Public Functions # --------------------------------------------------------------------------- -def get_version(file, name='__version__'): + +def get_version(file, name="__version__"): """Get the version of the package from the given file by executing it and extracting the given `name`. """ @@ -84,12 +89,11 @@ def get_version(file, name='__version__'): def ensure_python(specs): - """Given a list of range specifiers for python, ensure compatibility. - """ + """Given a list of range specifiers for python, ensure compatibility.""" if not isinstance(specs, (list, tuple)): specs = [specs] v = sys.version_info - part = '%s.%s' % (v.major, v.minor) + part = "%s.%s" % (v.major, v.minor) for spec in specs: if part == spec: return @@ -98,7 +102,7 @@ def ensure_python(specs): return except SyntaxError: pass - raise ValueError('Python version %s unsupported' % part) + raise ValueError("Python version %s unsupported" % part) def find_packages(top=HERE): @@ -107,8 +111,8 @@ def find_packages(top=HERE): """ packages = [] for d, dirs, _ in os.walk(top, followlinks=True): - if os.path.exists(pjoin(d, '__init__.py')): - packages.append(os.path.relpath(d, top).replace(os.path.sep, '.')) + if os.path.exists(pjoin(d, "__init__.py")): + packages.append(os.path.relpath(d, top).replace(os.path.sep, ".")) elif d != top: # Do not look for packages in subfolders if current is not a package dirs[:] = [] @@ -117,7 +121,7 @@ def find_packages(top=HERE): def update_package_data(distribution): """update build_py options to get package_data changes""" - build_py = distribution.get_command_obj('build_py') + build_py = distribution.get_command_obj("build_py") build_py.finalize_options() @@ -127,13 +131,15 @@ class bdist_egg_disabled(bdist_egg): Prevents setup.py install performing setuptools' default easy_install, which it should never ever do. """ + def run(self): - sys.exit("Aborting implicit building of eggs. Use `pip install .` " - " to install from source.") + sys.exit( + "Aborting implicit building of eggs. Use `pip install .` " + " to install from source." + ) -def create_cmdclass(prerelease_cmd=None, package_data_spec=None, - data_files_spec=None): +def create_cmdclass(prerelease_cmd=None, package_data_spec=None, data_files_spec=None): """Create a command class with the given optional prerelease class. Parameters @@ -167,11 +173,11 @@ def create_cmdclass(prerelease_cmd=None, package_data_spec=None, """ wrapped = [prerelease_cmd] if prerelease_cmd else [] if package_data_spec or data_files_spec: - wrapped.append('handle_files') + wrapped.append("handle_files") wrapper = functools.partial(_wrap_command, wrapped) handle_files = _get_file_handler(package_data_spec, data_files_spec) - if 'bdist_egg' in sys.argv: + if "bdist_egg" in sys.argv: egg = wrapper(bdist_egg, strict=True) else: egg = bdist_egg_disabled @@ -184,9 +190,9 @@ def create_cmdclass(prerelease_cmd=None, package_data_spec=None, ) if bdist_wheel: - cmdclass['bdist_wheel'] = wrapper(bdist_wheel, strict=True) + cmdclass["bdist_wheel"] = wrapper(bdist_wheel, strict=True) - cmdclass['develop'] = wrapper(develop, strict=True) + cmdclass["develop"] = wrapper(develop, strict=True) return cmdclass @@ -194,7 +200,6 @@ def command_for_func(func): """Create a command that calls the given function.""" class FuncCommand(BaseCommand): - def run(self): func() update_package_data(self.distribution) @@ -204,10 +209,10 @@ def run(self): def run(cmd, **kwargs): """Echo a command before running it. Defaults to repo as cwd""" - log.info('> ' + list2cmdline(cmd)) - kwargs.setdefault('cwd', HERE) - kwargs.setdefault('shell', os.name == 'nt') - if not isinstance(cmd, (list, tuple)) and os.name != 'nt': + log.info("> " + list2cmdline(cmd)) + kwargs.setdefault("cwd", HERE) + kwargs.setdefault("shell", os.name == "nt") + if not isinstance(cmd, (list, tuple)) and os.name != "nt": cmd = shlex.split(cmd) cmd[0] = which(cmd[0]) return subprocess.check_call(cmd, **kwargs) @@ -215,7 +220,7 @@ def run(cmd, **kwargs): def is_stale(target, source): """Test whether the target file/directory is stale based on the source - file/directory. + file/directory. """ if not os.path.exists(target): return True @@ -225,6 +230,7 @@ def is_stale(target, source): class BaseCommand(Command): """Empty command because Command needs subclasses to override too much""" + user_options = [] def initialize_options(self): @@ -260,6 +266,7 @@ def finalize_options(self): def run(self): for c in self.commands: c.run() + return CombinedCommand @@ -310,7 +317,9 @@ def mtime(path): return os.stat(path).st_mtime -def install_npm(path=None, build_dir=None, source_dir=None, build_cmd='build', force=False, npm=None): +def install_npm( + path=None, build_dir=None, source_dir=None, build_cmd="build", force=False, npm=None +): """Return a Command for managing an npm installation. Note: The command is skipped if the `--skip-npm` flag is used. @@ -331,40 +340,45 @@ def install_npm(path=None, build_dir=None, source_dir=None, build_cmd='build', f """ class NPM(BaseCommand): - description = 'install package.json dependencies using npm' + description = "install package.json dependencies using npm" def run(self): if skip_npm: - log.info('Skipping npm-installation') + log.info("Skipping npm-installation") return node_package = path or HERE - node_modules = pjoin(node_package, 'node_modules') - is_yarn = os.path.exists(pjoin(node_package, 'yarn.lock')) + node_modules = pjoin(node_package, "node_modules") + is_yarn = os.path.exists(pjoin(node_package, "yarn.lock")) npm_cmd = [npm] if isinstance(npm, str) else npm if npm is None: if is_yarn: - npm_cmd = ['yarn'] + npm_cmd = ["yarn"] else: - npm_cmd = ['npm'] + npm_cmd = ["npm"] if not which(npm_cmd[0]): - log.error("`{0}` unavailable. If you're running this command " - "using sudo, make sure `{0}` is available to sudo" - .format(npm_cmd[0])) + log.error( + "`{0}` unavailable. If you're running this command " + "using sudo, make sure `{0}` is available to sudo".format( + npm_cmd[0] + ) + ) return - if force or is_stale(node_modules, pjoin(node_package, 'package.json')): - log.info('Installing build dependencies with npm. This may ' - 'take a while...') - run(npm_cmd + ['install'], cwd=node_package) + if force or is_stale(node_modules, pjoin(node_package, "package.json")): + log.info( + "Installing build dependencies with npm. This may " + "take a while..." + ) + run(npm_cmd + ["install"], cwd=node_package) if build_dir and source_dir and not force: should_build = is_stale(build_dir, source_dir) else: should_build = True if should_build: - run(npm_cmd + ['run', build_cmd], cwd=node_package) + run(npm_cmd + ["run", build_cmd], cwd=node_package) return NPM @@ -380,11 +394,11 @@ def ensure_targets(targets): class TargetsCheck(BaseCommand): def run(self): if skip_npm: - log.info('Skipping target checks') + log.info("Skipping target checks") return missing = [t for t in targets if not os.path.exists(t)] if missing: - raise ValueError(('missing files: %s' % missing)) + raise ValueError(("missing files: %s" % missing)) return TargetsCheck @@ -403,8 +417,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): # Additionally check that `file` is not a directory, as on Windows # directories pass the os.access check. def _access_check(fn, mode): - return (os.path.exists(fn) and os.access(fn, mode) and - not os.path.isdir(fn)) + return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn) # Short circuit. If we're given a full path which matches the mode # and it exists, we're done here. @@ -458,10 +471,10 @@ def _wrap_command(cmds, cls, strict=True): strict: boolean, optional Whether to raise errors when a pre-command fails. """ - class WrappedCommand(cls): + class WrappedCommand(cls): def run(self): - if not getattr(self, 'uninstall', None): + if not getattr(self, "uninstall", None): try: [self.run_command(cmd) for cmd in cmds] except Exception: @@ -474,14 +487,14 @@ def run(self): result = cls.run(self) return result + return WrappedCommand def _get_file_handler(package_data_spec, data_files_spec): - """Get a package_data and data_files handler command. - """ - class FileHandler(BaseCommand): + """Get a package_data and data_files handler command.""" + class FileHandler(BaseCommand): def run(self): package_data = self.distribution.package_data package_spec = package_data_spec or dict() @@ -518,15 +531,15 @@ def _get_data_files(data_specs, existing): # Extract the files and assign them to the proper data # files path. for (path, dname, pattern) in data_specs or []: - dname = dname.replace(os.sep, '/') + dname = dname.replace(os.sep, "/") offset = len(dname) + 1 files = _get_files(pjoin(dname, pattern)) for fname in files: # Normalize the path. root = os.path.dirname(fname) - full_path = '/'.join([path, root[offset:]]) - if full_path.endswith('/'): + full_path = "/".join([path, root[offset:]]) + if full_path.endswith("/"): full_path = full_path[:-1] file_data[full_path].append(fname) @@ -566,13 +579,13 @@ def _get_files(file_patterns, top=HERE): for root, dirnames, filenames in os.walk(top): # Don't recurse into node_modules - if 'node_modules' in dirnames: - dirnames.remove('node_modules') + if "node_modules" in dirnames: + dirnames.remove("node_modules") for m in matchers: for filename in filenames: fn = os.path.relpath(pjoin(root, filename), top) if m(fn): - files.add(fn.replace(os.sep, '/')) + files.add(fn.replace(os.sep, "/")) return list(files) @@ -594,16 +607,16 @@ def _get_package_data(root, file_patterns=None): Files in `node_modules` are ignored. """ if file_patterns is None: - file_patterns = ['*'] + file_patterns = ["*"] return _get_files(file_patterns, pjoin(HERE, root)) def _compile_pattern(pat, ignore_case=True): """Translate and compile a glob pattern to a regular expression matcher.""" if isinstance(pat, bytes): - pat_str = pat.decode('ISO-8859-1') + pat_str = pat.decode("ISO-8859-1") res_str = _translate_glob(pat_str) - res = res_str.encode('ISO-8859-1') + res = res_str.encode("ISO-8859-1") else: res = _translate_glob(pat) flags = re.IGNORECASE if ignore_case else 0 @@ -632,9 +645,9 @@ def _translate_glob(pat): translated_parts = [] for part in _iexplode_path(pat): translated_parts.append(_translate_glob_part(part)) - os_sep_class = '[%s]' % re.escape(SEPARATORS) + os_sep_class = "[%s]" % re.escape(SEPARATORS) res = _join_translated(translated_parts, os_sep_class) - return '{res}\\Z(?ms)'.format(res=res) + return "{res}\\Z(?ms)".format(res=res) def _join_translated(translated_parts, os_sep_class): @@ -643,20 +656,20 @@ def _join_translated(translated_parts, os_sep_class): This is different from a simple join, as care need to be taken to allow ** to match ZERO or more directories. """ - res = '' + res = "" for part in translated_parts[:-1]: - if part == '.*': + if part == ".*": # drop separator, since it is optional # (** matches ZERO or more dirs) res += part else: res += part + os_sep_class - if translated_parts[-1] == '.*': + if translated_parts[-1] == ".*": # Final part is ** - res += '.+' + res += ".+" # Follow stdlib/git convention of matching all sub files/directories: - res += '({os_sep_class}?.*)?'.format(os_sep_class=os_sep_class) + res += "({os_sep_class}?.*)?".format(os_sep_class=os_sep_class) else: res += translated_parts[-1] return res @@ -665,36 +678,36 @@ def _join_translated(translated_parts, os_sep_class): def _translate_glob_part(pat): """Translate a glob PATTERN PART to a regular expression.""" # Code modified from Python 3 standard lib fnmatch: - if pat == '**': - return '.*' + if pat == "**": + return ".*" i, n = 0, len(pat) res = [] while i < n: c = pat[i] i = i + 1 - if c == '*': + if c == "*": # Match anything but path separators: - res.append('[^%s]*' % SEPARATORS) - elif c == '?': - res.append('[^%s]?' % SEPARATORS) - elif c == '[': + res.append("[^%s]*" % SEPARATORS) + elif c == "?": + res.append("[^%s]?" % SEPARATORS) + elif c == "[": j = i - if j < n and pat[j] == '!': + if j < n and pat[j] == "!": j = j + 1 - if j < n and pat[j] == ']': + if j < n and pat[j] == "]": j = j + 1 - while j < n and pat[j] != ']': + while j < n and pat[j] != "]": j = j + 1 if j >= n: - res.append('\\[') + res.append("\\[") else: - stuff = pat[i:j].replace('\\', '\\\\') + stuff = pat[i:j].replace("\\", "\\\\") i = j + 1 - if stuff[0] == '!': - stuff = '^' + stuff[1:] - elif stuff[0] == '^': - stuff = '\\' + stuff - res.append('[%s]' % stuff) + if stuff[0] == "!": + stuff = "^" + stuff[1:] + elif stuff[0] == "^": + stuff = "\\" + stuff + res.append("[%s]" % stuff) else: res.append(re.escape(c)) - return ''.join(res) + return "".join(res) diff --git a/tests/test-browser/run_browser_test.py b/tests/test-browser/run_browser_test.py index 553420286..e581bc612 100644 --- a/tests/test-browser/run_browser_test.py +++ b/tests/test-browser/run_browser_test.py @@ -9,6 +9,6 @@ here = os.path.abspath(os.path.dirname(__file__)) -if __name__ == '__main__': - with patch('jupyterlab.browser_check.here', here): - BrowserApp.launch_instance() \ No newline at end of file +if __name__ == "__main__": + with patch("jupyterlab.browser_check.here", here): + BrowserApp.launch_instance()