diff --git a/README.org b/README.org index 9befb5b..11d09ea 100644 --- a/README.org +++ b/README.org @@ -4872,6 +4872,232 @@ ruff "$@" | cat # End: #+end_src +*** [[https://pip.pypa.io/en/stable/][Package Installer for Python]] +:PROPERTIES: +:CUSTOM_ID: sec:pip-python +:header-args: :eval never-export +:END: + +#+caption[Show how to use =pip list --outdated=]: +#+caption: Show how to use =pip list --outdated=. +#+header: :wrap src text +#+name: lst:shell-pip-list-outdated +#+begin_src shell :exports both :results verbatim +# WARNING: "pip index" is currently an experimental command. +pip list --outdated +#+end_src + +#+caption[The result of =pip list --outdated=]: +#+caption: The result of =pip list --outdated=. +#+RESULTS: lst:shell-pip-list-outdated +#+begin_src text +Package Version Latest Type +------------------- ------- ------ ----- +aiofiles 22.1.0 23.1.0 wheel +jupyter_server_ydoc 0.6.1 0.7.0 wheel +jupyter-ydoc 0.2.2 0.3.4 wheel +y-py 0.5.9 0.6.0 wheel +ypy-websocket 0.8.2 0.8.4 wheel +#+end_src + +#+caption[Show how to use =pip index versions=]: +#+caption: Show how to use =pip index versions=. +#+header: :wrap src text +#+name: lst:shell-pip-index-versions +#+begin_src shell :exports both :results verbatim +# WARNING: "pip index" is currently an experimental command. +pip index versions circular-buffer --no-color +#+end_src + +#+caption[The result of =pip index versions=]: +#+caption: The result of =pip index versions=. +#+RESULTS: lst:shell-pip-index-versions +#+begin_src text +circular-buffer (0.2.0) +Available versions: 0.2.0, 0.1.1, 0.1.0 +#+end_src + +#+caption[Emacs interface to list outdated Python packages]: +#+caption: Emacs interface to list outdated Python packages. +#+name: lst:pip-list-outdated +#+begin_src emacs-lisp -n results silent +(defvar pip-outdated-packages nil + "Outdated Python packages.") + +(defun pip--list-outdated-sentinel (process event) + (when (and (eq (process-status process) 'exit) + (zerop (process-exit-status process)) + (buffer-live-p (process-buffer process))) + (with-current-buffer (process-buffer process) + (goto-char (point-min)) + (setq pip-outdated-packages + (json-parse-buffer :array-type 'list :object-type 'alist)) + (let ((alists pip-outdated-packages)) + (while alists + (setf (alist-get 'name (car alists)) + (string-replace "_" "-" (alist-get 'name (car alists)))) + (setq alists (cdr alists)))) + (kill-buffer) + (message "Calling `%S' succeeded" #'pip-list-outdated)))) + +(defun pip-list-outdated () + "Save the outdated Python packages in `pip-outdated-packages' + +This invokes an asynchonous process and finishes with a message." + (interactive) + (let ((command '("pip" "list" "--outdated" "--format" "json"))) + (make-process + :name "pip-list-outdated" + :buffer (generate-new-buffer-name "*pip-list-outdated-output*") + :command command + :sentinel #'pip--list-outdated-sentinel) + (message "Running `%s' asynchonously" (string-join command " ")))) +#+end_src + +#+caption[Emacs interface to upgrade outdated unfrozen Python packages]: +#+caption: Emacs interface to upgrade outdated unfrozen Python packages. +#+name: lst:pip-upgrade-maybe +#+begin_src emacs-lisp -n :results silent +(defcustom pip-frozen-packages '("aiofiles" + "jupyter-server-ydoc" + "jupyter-ydoc" + "y-py" + "ypy-websocket") + "Frozen Python packages (until jupyterlab-4.0.0)" + :type '(repeat string)) + +(defun pip--upgrade-maybe-sentinel (process event) + (when (and (eq (process-status process) 'exit) + (buffer-live-p (process-buffer process))) + (display-buffer (process-buffer process)))) + +(defun pip-upgrade-maybe () + "Install the latest version of outdated packages unless they are frozen. + +This invokes an asynchonous process and finishes with displaying the process +buffer to check whether upgrading has made the dependencies incompatible." + (interactive) + (let (found) + (dolist (alist pip-outdated-packages found) + (let ((name (alist-get 'name alist)) + (latest_version (alist-get 'latest_version alist))) + (unless (member name pip-frozen-packages) + (push (format "%s==%s" name latest_version) found)))) + (if (consp found) + (let ((command + `("pip" "install" "--progress-bar" "off" ,@(nreverse found)))) + (make-process + :name "pip-upgrade-maybe" + :buffer (generate-new-buffer-name "*pip-upgrade-maybe*") + :command command + :sentinel #'pip--upgrade-maybe-sentinel) + (message "Running `%s' asynchonously" (string-join command " "))) + (message "`pip-upgrade-maybe' found no packages to install")))) +#+end_src + +#+caption[Emacs interface to the =PyPI= simple =JSON= =API=]: +#+caption: Emacs interface to the =PyPI= simple =JSON= =API=. +#+name: lst:pip-pypi-simple-json-apip +#+begin_src emacs-lisp -n :results silent +(defun pip-simple-json-project-details (name) + (let ((url (format "https://pypi.org/simple/%s" name)) + (url-request-method "GET") + (url-mime-accept-string "application/vnd.pypi.simple.latest+json")) + (url-retrieve url + (lambda (status) (switch-to-buffer (current-buffer)))))) + +(defun pip-simple-json-project-list () + (interactive) + (let ((url-request-method "GET") + (url-mime-accept-string "application/vnd.pypi.simple.latest+json")) + (url-retrieve "https://pypi.org/simple" + (lambda (status) (switch-to-buffer (current-buffer)))))) +#+end_src + +#+caption[Emacs interface to get the dependency tree of installed packages]: +#+caption: Emacs interface to get the dependency tree of installed packages. +#+name: lst:pip-dependency-tree +#+begin_src emacs-lisp :results silent +(defvar pip-package-dependency-tree nil + "Python package dependency tree.") + +(defun pip--dependency-tree-sentinel (process event) + (when (and (eq (process-status process) 'exit) + (zerop (process-exit-status process)) + (buffer-live-p (process-buffer process))) + (with-current-buffer (process-buffer process) + (goto-char (point-min)) + (setq pip-package-dependency-tree + (json-parse-buffer :array-type 'list :object-type 'alist)) + (kill-buffer) + (message "Calling `%S' succeeded" #'pip-dependency-tree)))) + +(defun pip-dependency-tree () + "Save the Python package dependency tree in `pip-package-dependency-tree'." + (interactive) + (let ((command '("pipdeptree" "--json-tree"))) + (make-process + :name "pip-dependency-tree" + :buffer (generate-new-buffer-name "*pip-dependency-tree-output*") + :command command + :sentinel #'pip--dependency-tree-sentinel) + (message "Running `%s' asynchonously" (string-join command " ")))) +#+end_src + +#+caption[Emacs functions to flatten the Python package dependency tree]: +#+caption: Emacs functions to flatten the Python package dependency tree. +#+name: lst:pip-python-interface +#+begin_src emacs-lisp -n :results silent +(defun pip--flatten-dependencies (parents found) + (dolist (parent parents) + (when-let ((children (alist-get 'dependencies parent))) + (setq found (pip--flatten-dependencies children found))) + (if-let* ((pair `(key . ,(alist-get 'key parent))) + (old (cl-loop for alist in found thereis (member pair alist))) + (rvs (split-string (alist-get 'required_version parent) "[,]+"))) + (dolist (rv rvs) + (cl-pushnew rv (alist-get 'required_version old) :test #'equal)) + (let* ((new (assq-delete-all 'dependencies (copy-alist parent))) + (rvs (split-string (alist-get 'required_version new) "[,]+"))) + (setf (alist-get 'required_version new) rvs) + (push new found)))) + found) + +(defun pip-flatten-package-dependency-tree () + (let (found) + (dolist (package pip-package-dependency-tree) + (when-let ((children (alist-get 'dependencies package))) + (setq found (pip--flatten-dependencies children found)))) + found)) + +(defconst pip-canonical-version-regexp-alist + '(("^[.]dev$" . -4) + ("^a$" . -3) + ("^b$" . -2) + ("^rc$" . -1) + ("^[.]post$" . 1)) + "Specify association between canonical \"PEP-440\" version and its priority.") + +(defun pip-canonical-version-to-list (version) + (let ((version-regexp-alist pip-canonical-version-regexp-alist)) + (version-to-list version))) + +;; https://peps.python.org/pep-0440/ +(defun pip-pep-440 (clauses) + (let (compatibles exclusions greater-equals less-thans unknowns) + (dolist (clause clauses) + (cond ((string-prefix-p "~=" clause) + (push (substring clause 2) compatibles)) + ((string-prefix-p "!=" clause) + (push (substring clause 2) exclusions)) + ((string-prefix-p ">=" clause) + (push (substring clause 2) greater-equals)) + ((string-prefix-p "<" clause) + (push (substring clause 1) less-thans)) + (t (push clause unknowns)))) + (list compatibles exclusions greater-equals less-thans unknowns))) +#+end_src + *** [[https://github.com/joaotavora/eglot][Eglot]] for [[https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/progmodes/python.el][python-mode]] :PROPERTIES: :CUSTOM_ID: sec:eglot-python