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))