Skip to content

Instantly share code, notes, and snippets.

@jeff-phil
Last active July 30, 2025 06:06
Show Gist options
  • Select an option

  • Save jeff-phil/3220f06f5084b2ff21817dcf2bed49e7 to your computer and use it in GitHub Desktop.

Select an option

Save jeff-phil/3220f06f5084b2ff21817dcf2bed49e7 to your computer and use it in GitHub Desktop.
Emacs Python Mode setup with auto venv and eglot

Here's a somewhat simplified way to setup ​python-mode/python-ts-mode with eglot with automatic venv discovery not using emacs pet-mode, pyenv, pyvenv, etc. and does not require a specific version of eglot. It utilizes emacs built-in project package. If need projectile instead, let me know.

(use-package python
    :hook (python-base-mode . my/python-shared-hook-load)
    :config
    (defun my/get-python-venv (&optional dir)
    "Return the virtual environment directory based on optional `DIR' directory
    parameter, or look in current project root i, or default directory. Note if using
    projectile, please pass in `(project-root)' as `DIR' parameter."
    (interactive)
    (when-let* ((root (or dir
                         (project-root (project-current))
                         default-directory))
                (venv-root
                        (seq-find
                         (lambda (candidate)
                           (let ((path (concat (file-name-as-directory root) candidate)))
                             (and (file-exists-p path)
                                  (expand-file-name path))))
                         '(".venv" "venv" ".env" "env"))))
      (expand-file-name venv-root root)))

    (defun my/python-shared-hook-load ()
      (interactive)
      (when-let (venv-bin (expand-file-name "bin/" (my/get-python-venv)))
        ;; Add `.venv/bin' to exec-path for LSP servers, etc.
        (setq-local exec-path (cons venv-bin exec-path))
        (setq-local exec-path (cons (concat "VIRTUAL_ENV=" venv-bin) exec-path))
        (setq-local python-shell-virtualenv-root venv-bin)
        (setq-local python-shell-interpreter (executable-find "python")))
      (eglot-ensure)))

(use-package eglot
  :config
  ;; If don't want to just use jedi, comment or change below.  Personally I use
  ;; `basedpyright-langserver'
  (add-to-list 'eglot-server-programs '((python-mode python-ts-mode) . ("jedi-language-server")))
)

(use-package project
  :config
  (setq project-switch-commands 'project-dired)

  (defvar my/project-root-markers
    '("pyproject.toml" "requirements.txt" "poetry.lock" "setup.py" ".venv" "venv" "uv.lock"
      ".project" ".dir-locals.el" "cscope.out" "tags" "Makefile" ".*cmake"
      "Eldev" "Eask" "Cask" "Cargo.toml" "package.json" "gulpfile.js" "info.rkt"
      "project.clj" "pom.xml" "README.org" "README.md")
    "A list of file/directory names that indicate a project root.")

  (defun my/find-project-by-marker-or-vc (dir)
    "Find a project root by searching upwards for any of `my/project-root-markers'.
     This function is intended to be used in `project-find-functions`
     Searches for markers, then for vc indicator, then `default-directory'"
    (let ((start-dir (if (file-directory-p dir)
                         (file-name-as-directory dir)
                       (file-name-directory dir))))
      ;; Set the stop condition for locate-dominating-file
      (or (let ((locate-dominating-stop-dir-regexp
                 "\\`\\(?:[\\/][\\/][^\\/]+[\\/]\\|/\\(?:net\\|afs\\|\\.\\.\\.\\)/\\|~\\/\\)\\'"))
            ;; return directory where a marker was found
            (when-let ((found-dir (locate-dominating-file
                                   start-dir
                                   ;; predicate function, runs for each parent dir.
                                   (lambda (d)
                                     ;; checks marker list and stops at first match
                                     (seq-find (lambda (marker)
                                                 (file-exists-p (concat d marker)))
                                               my/project-root-markers)))))
              ;; If a directory was found, return it in the format project.el expects,
              ;; or look at project-try-vc
              (cons 'transient (expand-file-name found-dir))))
          (project-try-vc start-dir)
          default-directory)))

  (add-hook 'project-find-functions #'my/find-project-by-marker-or-vc))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment